package com.zbkj.client.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zbkj.client.config.WebSocketSessionManager;
import com.zbkj.client.enums.WebSocketEventType;
import com.zbkj.client.service.*;
import com.zbkj.client.task.VideoSplitUpdateStatusTask;
import com.zbkj.client.task.VideoSubtitleUpdateStatusTask;
import com.zbkj.client.task.VideoTemplateUpdateStatusTask;
import com.zbkj.client.utils.EmojiFilterUtil;
import com.zbkj.client.utils.FileUtil;
import com.zbkj.common.exception.CrmebException;
import com.zbkj.common.model.client.agent.WeekTask;
import com.zbkj.common.model.client.digital.CustomDigitalPerson;
import com.zbkj.common.model.client.digital.DpVoice;
import com.zbkj.common.model.client.digital.PublicDigitalPerson;
import com.zbkj.common.model.client.workbench.*;
import com.zbkj.common.request.client.ReplacePicRequest;
import com.zbkj.common.request.client.VideoTemplateRequest;
import com.zbkj.common.response.ClientLoginResponse;
import com.zbkj.common.result.CommonResult;
import com.zbkj.common.utils.OssUtil;
import com.zbkj.common.vo.client.CreateFolderVo;
import com.zbkj.service.service.*;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import com.zbkj.common.model.assets.Material;
import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import com.zbkj.common.model.client.digital.DgVideo;
import com.alibaba.fastjson.JSON;


@Slf4j
@Service
public class VideoServiceCommonImpl implements VideoServiceCommon {

    @Value("${workbench.video-real-url}")
    private String videoRealInfoBaseUrl;

    private final String douyinRealInfoUrl = "/api/hybrid/video_data";

    @Value("${workbench.video-split}")
    private String videoSplitBaseUrl;
    private final String videoSplitUrl = "/api/v1/video/dismantle";
    private final String videoSubtitleUrl = "/api/v1/video/subtitle";
    private final String videoSubtitleUrlNew = "/api/v1/video/split";

    @Value("${workbench.agent}")
    private String agentBaseUrl;

    @Value("${workbench.hot}")
    private String hotBaseUrl;

    @Resource
    private RestTemplate restTemplate;

    @Autowired
    private VideoSplitUpdateStatusTask videoSplitUpdateStatusTask;

    @Autowired
    private VideoSubtitleUpdateStatusTask videoSubtitleUpdateStatusTask;

    @Autowired
    private VideoSplitService videoSplitService;

    @Autowired
    private VideoSubtitleService videoSubtitleService;

    @Autowired
    private VideoSplitUrlHistoryService videoSplitUrlHistoryService;

    @Autowired
    private MaterialService materialService;

    @Autowired
    private DgVideoService dgVideoService;

    @Autowired
    private DpVoiceService dpVoiceService;

    @Autowired
    private DigitalPersonService digitalPersonService;

    @Resource
    private ThreadPoolTaskScheduler taskScheduler;

    @Autowired
    private VideoTemplateFinalService videoTemplateFinalService;

    @Autowired
    private VideoTemplateConfigService videoTemplateConfigService;

    @Autowired
    private CustomDigitalPersonService  customDigitalPersonService;

    @Autowired
    private VideoTemplateUpdateStatusTask videoTemplateUpdateStatusTask;

    @Autowired
    private WebSocketSessionManager webSocketSessionManager;

    @Autowired
    private WeekTaskService weekTaskService;

    @Autowired
    private AgentClientService agentClientService;

    @Autowired
    private ClientLoginService clientLoginService;

    @Autowired
    private PublicDigitalPersonService publicDigitalPersonService;


    /**
     * 热榜列表
     * @param source
     */
    public List<Map<String, String>> getHostList(String source){
        String requestUrl = hotBaseUrl + "/" + source;

        List<Map<String, String>> resultList = new ArrayList<>();

        HttpHeaders headers = new HttpHeaders();
        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(headers);
        ResponseEntity<Map> response = restTemplate.exchange(requestUrl, HttpMethod.GET, entity, Map.class);
        Map<String, Object> respBody = response.getBody();
        if (respBody != null && Integer.valueOf(200).equals(respBody.get("code"))) {
            // data.video.download_addr.url_list [] 数组形式
            List<Map<String, Object>> data = (List<Map<String, Object>>) respBody.get("data");

            for (Map<String, Object> map : data) {
                HashMap<String , String> hashMap = new HashMap<>();
                hashMap.put("title", (String) map.get("title"));
                if ("weibo".equals(source)){
                    hashMap.put("content", (String) map.get("desc"));
                }else{
                    hashMap.put("content", (String) map.get("title"));
                }

                resultList.add(hashMap);
            }

            return resultList;
        } else {
            throw new CrmebException(respBody != null ? respBody.get("message").toString() : "获取视频真实信息失败");
        }
    }


    @Override
    public Map<String, Object> getVideoRealInfo(String url) {
        if (url == null || !url.contains("douyin.com")) {
            throw new CrmebException("仅支持抖音链接解析，请输入有效的抖音视频链接");
        }
        String source = "douyin";

//        String requestUrl = videoRealInfoBaseUrl + douyinRealInfoUrl + "?minimal=false&url=" + url;
//        log.info("获取抖音请求的地址为：" + requestUrl);
//        HttpHeaders headers = new HttpHeaders();
//        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(headers);

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Map<String, Object> body = new HashMap<>();
        body.put("url", url);
        body.put("minimal", false);

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

        String requestUrl = videoRealInfoBaseUrl + douyinRealInfoUrl;
        ResponseEntity<Map> response = restTemplate.exchange(requestUrl, HttpMethod.POST, entity, Map.class);
        Map<String, Object> respBody = response.getBody();
        if (respBody != null && Integer.valueOf(200).equals(respBody.get("code"))) {
            // data.video.download_addr.url_list [] 数组形式
            Map<String, Object> data = (Map<String, Object>) respBody.get("data");
            String videoUrl="";
            String format="";
            String author="";
            String title="";
            Integer diggCount=0; //喜欢
            Integer commentCount=0; //评论
            Integer collectCount=0; //收藏
            Integer duration=0; //时长

            if (data != null) {
                Map<String, Object> video =  (Map<String, Object>) data.get("video");
                if (video != null) {
                    format = (String) video.get("format");

                    Map<String, Object> downloadAddr =  (Map<String, Object>) video.get("download_addr");
                    if (downloadAddr != null) {
                        List urlList = (List) downloadAddr.get("url_list");
                        if (urlList != null) {
                            //只能选https://www.douyin.com/aweme/v1/play/ 这个地址的，其他地址的，要不然打不开，要不然不能访问
                            videoUrl = urlList.get(2).toString();
                        }
                    }
                }

                //取抖音的作者
                Map<String, Object> authors =  (Map<String, Object>) data.get("author");
                if (authors != null) {
                    author = (String) authors.get("nickname");
                }

                //取抖音的标题
                title =  (String) data.get("desc");
                int durationInt =  (Integer) data.get("duration");

                duration = (int) Math.ceil((double) durationInt / 1000);

                //取抖音的点赞数，收藏数，评论数
                Map<String, Object> statistics =  (Map<String, Object>) data.get("statistics");
                if (statistics != null) {
                    diggCount = (Integer) statistics.get("digg_count");
                    commentCount = (Integer) statistics.get("comment_count");
                    collectCount = (Integer) statistics.get("collect_count");
                }
            }

            HashMap<String, Object> result = new HashMap<>();
            result.put("videoUrl", videoUrl);
            result.put("format", format);
            result.put("source", source);
            result.put("author", author);
            result.put("title", title);
            result.put("diggCount", diggCount);
            result.put("commentCount", commentCount);
            result.put("collectCount", collectCount);
            result.put("duration", duration);

            return result;
        } else {
            throw new CrmebException(respBody != null ? respBody.get("message").toString() : "获取视频真实信息失败");
        }
    }

    /**
     * 视频拆解
     * @param videoUrl 视频链接
     * @return
     */
    @Override
    public Map<String, Object> videoSplitApi(String videoUrl) {
        String url = videoSplitBaseUrl + videoSplitUrl;
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Map<String, Object> body = new HashMap<>();
        body.put("video_url", videoUrl);

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
        ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);

        Map<String, Object> respBody = response.getBody();
        if (respBody == null) {
            throw new CrmebException("视频拆解API响应为空");
        }
        return respBody;
    }

    /**
     * 视频拆解
     * @param videoUrl 视频链接
     * @return
     */
    @Override
    public Map<String, Object> videoSubtitleApi(String videoUrl) {
        String url = videoSplitBaseUrl + videoSubtitleUrlNew;
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Map<String, Object> body = new HashMap<>();
        body.put("video_url", videoUrl);

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
        ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);

        Map<String, Object> respBody = response.getBody();
        if (respBody == null) {
            throw new CrmebException("视频字幕API响应为空");
        }
        return respBody;
    }

    /**
     * 视频拆解
     * @param videoUrl 视频链接
     * @return
     */
    public Map<String, Object> videoSubtitleApiOld(String videoUrl) {
        String url = videoSplitBaseUrl + videoSubtitleUrl;
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Map<String, Object> body = new HashMap<>();
        body.put("media_url", videoUrl);

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
        ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);

        Map<String, Object> respBody = response.getBody();
        if (respBody == null) {
            throw new CrmebException("视频字幕API响应为空");
        }
        return respBody;
    }

    /**
     *
     * @param url 原始url
     * @param userId 用户id
     * @param merId 商户id
     * @return
     */
    @Override
    public Map<String, Object> douyinVideoSplitByUrl(String url, Integer userId, Integer merId, String type) {
        // 如果url以/结尾，则去掉
        if (url != null && url.endsWith("/")) {
            url = url.substring(0, url.length() - 1);
        }
//        //如果有相同的url，则只需要直接使用之前的解析过的url即可。
//        VideoSplitUrlHistory historyOld = videoSplitUrlHistoryService.lambdaQuery()
//                .eq(VideoSplitUrlHistory::getOriginalUrl, url)
//                .last("limit 1")
//                .one();
//
//        //如果是之前根据该url解析过，则直接将之前的解析记录复制过来
//        if(historyOld != null) {
//            VideoSplitUrlHistory historyNew = new VideoSplitUrlHistory();
//            historyNew.setUrl(historyOld.getUrl());
//            historyNew.setOssUrl(historyOld.getOssUrl()); //看看要不要复制一份OSS文件到自己目录下
//            historyNew.setOriginalUrl(historyOld.getOriginalUrl());
//            historyNew.setType(type);
//            historyNew.setUserId(userId);
//            historyNew.setMerId(merId);
//            historyNew.setSource(historyOld.getSource());
//            historyNew.setAuthor(historyOld.getAuthor());
//            historyNew.setDiggCount(historyOld.getDiggCount());
//            historyNew.setCollectCount(historyOld.getCollectCount());
//            historyNew.setCommentCount(historyOld.getCommentCount());
//            historyNew.setIsDel(0);
//            historyNew.setCreateTime(new Date());
//            historyNew.setUpdateTime(new Date());
//            videoSplitUrlHistoryService.save(historyNew);
//            // 获取刚插入的ID（假设history的ID是自增主键且save后会自动回填）
//            Integer insertedId = historyNew.getId();
//
//            HashMap<String, Object> result = new HashMap<>();
//            result.put("originalUrl", url);
//            result.put("url", historyOld.getUrl());
//            result.put("ossUrl", historyOld.getOssUrl());
//            result.put("source", historyOld.getSource());
//            result.put("historyId", insertedId);
//            return result;
//        }

        // 1. 调用视频真实地址
        Map<String, Object> data = getVideoRealInfo(url);
        if (data == null) {
            throw new CrmebException("未获取到正常url");
        }
        String videoUrl = (String) data.get("videoUrl");
        String source =  (String)data.get("source");
        String format =  (String)data.get("format");

        String author =  (String)data.get("author");
        String title =  (String)data.get("title");
        Integer diggCount =  (Integer)data.get("diggCount");
        Integer commentCount =  (Integer)data.get("commentCount");
        Integer collectCount =  (Integer)data.get("collectCount");
        Integer duration =  (Integer)data.get("duration");

        if (videoUrl == null) {
            throw new CrmebException("拆解结果未包含wm_video_url");
        }
        // 2. 上传到OSS
        String fileName = System.currentTimeMillis() + "." + format;
        String ossUrl = OssUtil.uploadFromUrlToOss(videoUrl, fileName, userId, merId, "video", "videosplit", "history");
        if (ossUrl == null) {
            throw new CrmebException("视频上传OSS失败");
        }
        // 3. 插入历史表
        VideoSplitUrlHistory history = new VideoSplitUrlHistory();
        history.setUrl(videoUrl);
        history.setOssUrl(ossUrl);
        history.setOriginalUrl(url);
        history.setType(type);
        history.setUserId(userId);
        history.setDuration(duration);
        history.setMerId(merId);
        history.setSource(source);
        history.setTitle(title);
        history.setIsDel(0);
        history.setAuthor(author);
        history.setDiggCount(diggCount);
        history.setCollectCount(collectCount);
        history.setCommentCount(commentCount);
        history.setCreateTime(new Date());
        history.setUpdateTime(new Date());
        videoSplitUrlHistoryService.save(history);
        // 获取刚插入的ID（假设history的ID是自增主键且save后会自动回填）
        Integer insertedId = history.getId();

        HashMap<String, Object> result = new HashMap<>();
        result.put("originalUrl", url);
        result.put("url", videoUrl);
        result.put("ossUrl", ossUrl);
        result.put("source", source);
        result.put("historyId", insertedId);
        result.put("duration", duration);
        return result;
    }

    /**
     * 从素材库上传进行视频拆解
     * @param materialId 素材ID
     * @param userId 用户ID
     * @param merId 商户ID
     * @return
     */
    @Override
    public Map<String, Object> VideoSplitByMaterial(Integer materialId, Integer userId, Integer merId, String type) {
        // 1. 获取素材信息
        Material material = materialService.getById(materialId);
        if (material == null || material.getIsDel() != null && material.getIsDel() == 1) {
            throw new CrmebException("素材不存在或已被删除");
        }
        String videoUrl = material.getUrl();
        if (videoUrl == null || videoUrl.isEmpty()) {
            throw new CrmebException("素材URL为空");
        }
        // 2. 拷贝到新OSS位置
        String fileName = videoUrl.substring(videoUrl.lastIndexOf('/') + 1);
        String ossUrl = OssUtil.copyOssFile(videoUrl, "history", merId, userId, "videosplit", fileName);
        if (ossUrl == null) {
            throw new CrmebException("视频拷贝到OSS失败");
        }
        // 3. 插入历史表
        VideoSplitUrlHistory history = new VideoSplitUrlHistory();
        history.setOriginalUrl(videoUrl);
        history.setUrl(videoUrl);
        history.setOssUrl(ossUrl);
        history.setUserId(userId);
        history.setMaterialId(materialId);
        history.setMerId(merId);
        history.setSource("material");
        history.setType(type);
        history.setDuration(material.getDuration());
        history.setIsDel(0);
        history.setCreateTime(new Date());
        history.setUpdateTime(new Date());
        videoSplitUrlHistoryService.save(history);

        // 获取刚插入的ID（假设history的ID是自增主键且save后会自动回填）
        Integer insertedId = history.getId();

        HashMap<String, Object> result = new HashMap<>();
        result.put("url", ossUrl);
        result.put("ossUrl", ossUrl);
        result.put("duration", material.getDuration());
        result.put("historyId", insertedId);

        return result;
    }

    /**
     * 通用视频拆解入口
     * @param type 类型 url/material
     * @param url 视频url
     * @param materialId 素材ID
     * @param userId 用户ID
     * @param merId 商户ID
     * @return
     */
    @Override
    public String VideoSplit(String type, String url, Integer materialId, Integer userId, Integer merId) {
        String videoUrl;
        String source;
        Integer historyId;
        Integer duration;
        if ("url".equalsIgnoreCase(type)) {
            Map<String, Object> videoResult = douyinVideoSplitByUrl(url, userId, merId, "split");
            videoUrl = (String) videoResult.get("url");
            source = (String) videoResult.get("source");
            historyId = (Integer) videoResult.get("historyId");
            duration = (Integer) videoResult.get("duration");
        } else if ("material".equalsIgnoreCase(type)) {
            Map<String, Object> videoResult = VideoSplitByMaterial(materialId, userId, merId, "split");
            videoUrl = (String) videoResult.get("url");
            historyId = (Integer) videoResult.get("historyId");
            duration = (Integer) videoResult.get("duration");
            source = "material";
        } else {
            throw new CrmebException("type参数错误，只能为url或material");
        }

        // 调用视频拆解接口
        Map<String, Object> respBody = videoSplitApi(videoUrl);
        if (respBody != null && Boolean.TRUE.equals(respBody.get("success"))) {
            Map<String, Object> data = (Map<String, Object>) respBody.get("data");
            String taskId = (String) data.get("task_id");

            // 插入数据库
            VideoSplit videoSplit = new VideoSplit();
            videoSplit.setUserId(userId);
            videoSplit.setMerId(merId);
            videoSplit.setLinkId(taskId);
            videoSplit.setSource(source);
            videoSplit.setDuration(duration);
            videoSplit.setHistoryId(historyId);
//            videoSplit.setMaterialId(materialId);
            videoSplit.setCreateTime(new Date());
            videoSplitService.save(videoSplit);

            Map<Object, Object> result = new HashMap<>();
            result.put("linkId", taskId);

            // 启动定时任务
            videoSplitUpdateStatusTask.start(taskId);
        } else {
            // return CommonResult.failed(respBody != null ? respBody.get("message").toString() : "视频拆解失败");
        }
        // 4. 返回视频拆解结果
        return videoUrl;
    }


    /**
     * 通用视频生成文案
     * @param type 类型 url/material
     * @param url 视频url
     * @param materialId 素材ID
     * @param userId 用户ID
     * @param merId 商户ID
     * @return
     */
    @Override
    public String VideoSubtitle(String type, String url, Integer materialId, Integer userId, Integer merId) {
        String videoUrl;
        String source;
        String title;
        Integer historyId;
        Integer duration;

        if ("url".equalsIgnoreCase(type)) {
            Map<String, Object> videoResult = douyinVideoSplitByUrl(url, userId, merId, "subtitle");
            videoUrl = (String) videoResult.get("ossUrl");
            source = (String) videoResult.get("source");
            title = (String) videoResult.get("title");
            historyId = (Integer) videoResult.get("historyId");
            duration = (Integer) videoResult.get("duration");
        } else if ("material".equalsIgnoreCase(type)) {
            Map<String, Object> videoResult = VideoSplitByMaterial(materialId, userId, merId, "subtitle");
            videoUrl = (String) videoResult.get("ossUrl");
            historyId = (Integer) videoResult.get("historyId");
            source = "material";
            duration = (Integer) videoResult.get("duration");
            title = "";
        } else {
            throw new CrmebException("type参数错误，只能为url或material");
        }

        // 调用视频拆解接口
        Map<String, Object> respBody = videoSubtitleApi(videoUrl);
        if (respBody != null && Boolean.TRUE.equals(respBody.get("success"))) {
            Map<String, Object> data = (Map<String, Object>) respBody.get("data");
            String taskId = (String) data.get("request_id");

            // 插入数据库
            VideoSubtitle videoSubtitle = new VideoSubtitle();
            videoSubtitle.setUserId(userId);
            videoSubtitle.setMerId(merId);
            videoSubtitle.setLinkId(taskId);
            videoSubtitle.setSource(source);
            videoSubtitle.setTitle(title);
            videoSubtitle.setDuration(duration);
            videoSubtitle.setHistoryId(historyId);
//            videoSplit.setMaterialId(materialId);
            videoSubtitle.setCreateTime(new Date());
            videoSubtitleService.save(videoSubtitle);

            Map<Object, Object> result = new HashMap<>();
            result.put("linkId", taskId);

            // 启动定时任务
            videoSubtitleUpdateStatusTask.start(taskId);
        } else {
            // return CommonResult.failed(respBody != null ? respBody.get("message").toString() : "视频拆解失败");
        }
        // 4. 返回视频拆解结果
        return videoUrl;
    }

    /**
     * 调用agent返回视频字幕和图片名称
     * @param content 内容
     * @return
     */
    @Override
    public Map<String , Object> videoSubtitleAgentApi(String content) {
        String url = agentBaseUrl + "/api/v1/agents/8584ad43-826f-442d-9c22-0a14a8244c0f/chat";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Map<String, Object> body = new HashMap<>();
        body.put("message", content);
        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

        //定义异常返回的内容
        Map<String, Object> map = new HashMap<>();

        ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
        Map<String, Object> respBody = response.getBody();
        if (respBody == null) {
            throw new CrmebException("agent接口响应为空");
        }
        Object codeObj = respBody.get("code");
        if (codeObj != null && Integer.valueOf(200).equals(codeObj)) {
            Map<String, Object> data = (Map<String, Object>) respBody.get("data");
            if (data != null && data.get("message") != null) {
                String messageStr = data.get("message").toString();
                JSONObject messageJson = JSONObject.parseObject(messageStr);
                Map<String , Object> json = (Map<String , Object>)JSONObject.toJSON(messageJson);
                return json;
//                return data.get("message");
            } else {
                map.put("msg", "agent接口返回数据格式异常");
                log.error("agent接口返回格式异常，返回内容为：{} ",respBody);
                return map;
            }
        } else {
            String msg = respBody.get("message").toString();
            map.put("msg", msg);
            log.error("agent接口返回错误信息：{} ",msg);
            return map;
        }
    }

    /**
     * 调用agent返回视频字幕和图片名称
     * @param content 内容
     * @return
     */
    @Override
    public Map<String , Object> videoSubtitleAgentApiNew(String content) {
        /**
         * {"title":{"content":"String","highlight":["String"]},"subtitle":[{"content":"String","highlight":["String"],"from":"Integer","to":"Integer"},{"content":"String","highlight":["String"],"from":"Integer","to":"Integer"}],"pictureInPicture":[{"hotspot":"String","time_start":"Integer","time_end":"Integer"},{"hotspot":"String","time_start":"Integer","time_end":"Integer"}]}
         */

        // JSON字符串
        String jsonString = "{\"title\":{\"content\":\"String\",\"highlight\":[\"String\"]},\"subtitle\":[{\"content\":\"String\",\"highlight\":[\"String\"],\"from\":\"Integer\",\"to\":\"Integer\"},{\"content\":\"String\",\"highlight\":[\"String\"],\"from\":\"Integer\",\"to\":\"Integer\"}],\"pictureInPicture\":[{\"hotspot\":\"String\",\"time_start\":\"Integer\",\"time_end\":\"Integer\"},{\"hotspot\":\"String\",\"time_start\":\"Integer\",\"time_end\":\"Integer\"}]}";

        // 转换为Map
        JSONObject jsonObject = JSON.parseObject(jsonString);
        Map<String, Object> outputFormatMap = (Map<String, Object>) JSON.toJSON(jsonObject);

        HashMap<String, Object> hashMap = new HashMap<>();
        hashMap.put("outputFormat", outputFormatMap);

        List<String> instructions = new ArrayList<>();
        instructions.add("根据字幕理解文案");
        instructions.add("为文案总结生成标题，不超过14个字");
        instructions.add("提取字幕中的关键词，用于合成视频时高亮用");
        // instructions.add("结合多个字幕总结内容后，提供在合适的片段（多个字幕组成一个连续片段）中插入画中画建议，开头几段字幕最好能提炼出一个画中画，如果字幕中有提到名人的话可应该适当的进行提炼整合，这样可以更能吸引用户");
        instructions.add("响应结构字段描述： title.content: 14个字以内的标题 title.highlight: 标题高亮关键词列表，不超过2个 subtitle.content: 字幕内容 subtitle.highlight: 字幕高亮关键词列表，不超过2个 subtitle.from: 字幕开始时间（第几秒开始) subtitle.to: 字幕结束时间（第几秒结束) pictureInPicture: 画中画建议，注意：每一个画中画建议，一般是由多个连续的字幕提炼而成 pictureInPicture.hotspot: 用来搜索画中画素材的关键词 pictureInPicture.time_start: 画中画开始时间（第几秒开始） pictureInPicture.time_end: 画中画结束时间（第几秒结束）");

        Map<String, Object> map = agenticAgentApi(content, hashMap, instructions,null, null);
        log.info("返回的结构是：{}", map);
        if (map != null && map.get("msg") == null) {
            return (Map<String , Object>)map.get("result");
        }

        return map;
    }

    /**
     * 调用agent返回标题
     * @param content 内容
     * @return
     */
    @Override
    public Map<String , Object> agenticAgentApi(String content, Map<String, Object> outputFormat, List<String> instructions, String agent_id, Map<String, Object> agentUserInfo) {
        String url = agentBaseUrl + "/api/v1/agents/agentic";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        //拼接结构化的参数
        Map<String, Object> body = new HashMap<>();
        body.put("instructions", instructions);
        body.put("message", content);

        if(agent_id == null || agent_id.isEmpty()){
            // 使用模型 结构化输出
            body.put("model_id", "8b407a9d-0c76-49f1-8dcd-840b8e29f886");
        }else{
            // 使用代理 结构化输出
            body.put("agent_id", agent_id);
        }

        //传用户信息
        if (agentUserInfo != null) {
            //用户信息可以传空
            body.put("user_info", agentUserInfo);
        }

        Map<String, Double> option = new HashMap<>();
        option.put("temperature", 0.7);
        body.put("options", option);
        body.put("output_format", outputFormat.get("outputFormat"));

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

        //定义异常返回的内容
        Map<String, Object> map = new HashMap<>();

        try{
            ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
            Map<String, Object> respBody = response.getBody();
            if (respBody == null) {
                throw new CrmebException("agent接口响应为空");
            }
            Object codeObj = respBody.get("code");
            if (codeObj != null && Integer.valueOf(200).equals(codeObj)) {
                Map<String, Object> data = (Map<String, Object>) respBody.get("data");
                if (data != null) {
//                String messageStr = data.get("message").toString();
//                JSONObject messageJson = JSONObject.parseObject(messageStr);
//                Map<String , Object> json = (Map<String , Object>)JSONObject.toJSON(messageJson);
                    return data;
//                return data.get("message");
                } else {
                    map.put("msg", "agent接口返回数据格式异常");
                    log.error("agent接口返回格式异常，返回内容为：{} ",respBody);
                    return map;
                }
            } else {
                String msg = respBody.get("message").toString();
                map.put("msg", msg);
                log.error("agent接口返回错误信息：{} ",msg);
                return map;
            }
        }catch (Exception e){
            String msg = "agent接口返回异常:" + e.getMessage().toString();
            map.put("msg", msg);
            log.error("agent接口返回格式异常，返回内容为：{} ",msg);
            return map;
        }
    }


    /**
     * 调用agent返回标题
     * @param content 内容
     * @return
     */
    @Override
    public Map<String , Object> videoTitle(String content) {
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("title", "String");

        HashMap<String, Object> output = new HashMap<>();
        output.put("outputFormat", hashMap);

        List<String> instructions = new ArrayList<>();

        Map<String, Object> map = agenticAgentApi(content, output, instructions,null, null);
        if (map != null && map.get("msg") == null) {

            return (Map<String , Object>)map.get("result");
        }

        return map;
    }

    /**
     * 调用搜索图片接口
     * @param picsKeywords 图片关键词数组
     * @return
     */
    @Override
    public List<Map<String, String>> videoPicAgentApi(List<Map<String, Object>> picsKeywords) {
        List<Map<String, String>> picUrls = new ArrayList<>();
        if (picsKeywords == null || picsKeywords.isEmpty()) {
            return null;
        }
        String url = videoSplitBaseUrl + "/api/v1/material/search";
        for (Map<String, Object> keywords : picsKeywords) {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            Map<String, Object> body = new HashMap<>();
            body.put("search_query", keywords.get("hotspot"));
            body.put("search_type", "image");
            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

            //data.Payload.Output.AgentContext.BizContext.GeneratedContent.ImageSearchResult.SearchResult,
            ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
            Map<String, Object> respBody = response.getBody();
            if (respBody != null && Boolean.TRUE.equals(respBody.get("success"))) {
                Map<String, Object> data = (Map<String, Object>) respBody.get("data");
                if (data != null) {
                    Map<String, Object> header = (Map<String, Object>) data.get("Header");
                    if (header != null && "task-finished".equals(header.get("Event"))) {
                        Map<String, Object> payload = (Map<String, Object>) data.get("Payload");
                        if (payload != null) {
                            Map<String, Object> output = (Map<String, Object>) payload.get("Output");
                            if (output != null) {
                                Map<String, Object> agentContext = (Map<String, Object>) output.get("AgentContext");
                                if (agentContext != null) {
                                    Map<String, Object> bizContext = (Map<String, Object>) agentContext.get("BizContext");
                                    if (bizContext != null) {
                                        Map<String, Object> generatedContent = (Map<String, Object>) bizContext.get("GeneratedContent");
                                        if (generatedContent != null) {
                                            Map<String, Object> imageSearchResult = (Map<String, Object>) generatedContent.get("ImageSearchResult");
                                            if (imageSearchResult != null) {
                                                List<Map<String, Object>> searchResult = (List<Map<String, Object>>) imageSearchResult.get("SearchResult");
                                                if (searchResult != null && !searchResult.isEmpty()) {
                                                    Object fileUrlObj = searchResult.get(0).get("FileUrl");
                                                    if (fileUrlObj != null) {
                                                        HashMap<String, String> map = new HashMap<>();
                                                        map.put("imgUrl", fileUrlObj.toString());
                                                        map.put("keyword", keywords.get("hotspot").toString());
                                                        map.put("from", keywords.get("time_start") == null ? null : keywords.get("time_start").toString());
                                                        map.put("to", keywords.get("time_end") == null ? null : keywords.get("time_end").toString());;
                                                        picUrls.add(map);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return picUrls;
    }

    /**
     * 调用搜索图片接口
     * @param picsKeywords 图片关键词数组
     * @return
     */
    @Override
    public List<Map<String, String>> videoPicMetaApi(List<Map<String, Object>> picsKeywords) {
        List<Map<String, String>> picUrls = new ArrayList<>();
        if (picsKeywords == null || picsKeywords.isEmpty()) {
            return null;
        }
        String url = videoSplitBaseUrl + "/api/v1/metaso/image-search";
        for (Map<String, Object> keywords : picsKeywords) {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            Map<String, Object> body = new HashMap<>();
            body.put("prompt", keywords.get("hotspot"));

            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

            //data.Payload.Output.AgentContext.BizContext.GeneratedContent.ImageSearchResult.SearchResult,
            ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
            Map<String, Object> respBody = response.getBody();
            if (respBody != null && Boolean.TRUE.equals(respBody.get("success"))) {
                Map<String, Object> data = (Map<String, Object>) respBody.get("data");
                if (data != null) {
                    Map<String, Object> searchResults = (Map<String, Object>) data.get("search_results");

                    if (searchResults != null) {
                        List<Map<String, Object>> images = (List<Map<String, Object>>) data.get("images");

                        if (images != null && !images.isEmpty()) {
                            String fileUrlObj = (String) images.get(0).get("imageUrl");
                            if (fileUrlObj != null) {
                                Map<String, String> map = new HashMap<>();
                                map.put("imgUrl", fileUrlObj.toString());
                                map.put("keyword", keywords.get("hotspot").toString());
                                map.put("from", keywords.get("time_start") == null ? null : keywords.get("time_start").toString());
                                map.put("to", keywords.get("time_end") == null ? null : keywords.get("time_end").toString());;
                                picUrls.add(map);
                            }
                        }
                    }
                }
            }
        }

        return picUrls;
    }

    /**
     * 调用搜索图片接口
     * @param picsKeywords 图片关键词数组
     * @return
     */
    @Override
    public List<Map<String, String>> picSearchApi(List<Map<String, Object>> picsKeywords) {
        List<Map<String, String>> picUrls = new ArrayList<>();
        if (picsKeywords == null || picsKeywords.isEmpty()) {
            return null;
        }
        String url = videoSplitBaseUrl + "/api/v1/smart-image/generate";
        for (Map<String, Object> keywords : picsKeywords) {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            Map<String, Object> body = new HashMap<>();
            body.put("prompt", keywords.get("hotspot"));

            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);

            //data.Payload.Output.AgentContext.BizContext.GeneratedContent.ImageSearchResult.SearchResult,
            ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
            Map<String, Object> respBody = response.getBody();
            if (respBody != null && Boolean.TRUE.equals(respBody.get("success"))) {
                Map<String, Object> data = (Map<String, Object>) respBody.get("data");
                if (data != null) {
                    String searchResult = (String) data.get("image_url");

                    if(searchResult != null) {
                        Map<String, String> map = new HashMap<>();
                        map.put("imgUrl", searchResult);
                        map.put("keyword", keywords.get("hotspot").toString());
                        map.put("from", keywords.get("time_start") == null ? null : keywords.get("time_start").toString());
                        map.put("to", keywords.get("time_end") == null ? null : keywords.get("time_end").toString());;
                        picUrls.add(map);
                    }
                }
            }
        }

        return picUrls;
    }

    /**
     * 模板成片-调用模板成片API
     * @param id 视频的id
     * @param dpVideoId 合成视频的id
     * @param picsInPic 画中画素材数组
     * @param userId 用户id
     * @param merId 商户id
     * @param subtitleAgentRes 字幕agent返回的内容
     * @return
     */
    @Override
    public String videoTemplateAgentApi(Integer id, Integer dpVideoId, List<Map<String, String>> picsInPic, Integer userId, Integer merId, Map<String, Object> subtitleAgentRes, String outputUrl) {
        // 根据dpVideoId查找数字人视频记录
        DgVideo dgVideo = dgVideoService.getById(dpVideoId);
        if (dgVideo == null) {
            throw new CrmebException("未找到对应的数字人视频记录");
        }
        String mediaUrl = dgVideo.getOssVideoUrl();
        if (mediaUrl == null || mediaUrl.isEmpty()) {
            throw new CrmebException("数字人视频OSS地址为空");
        }

        //获取视频模板信息
        VideoTemplateFinal videoTemplateFinal = videoTemplateFinalService.getById(id);

        // 调用模板成片接口
        String url = videoSplitBaseUrl + "/api/v1/video/template";
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Map<String, Object> body = new HashMap<>();
        body.put("media_url", mediaUrl);

        //拼接画中画素材
        List<Map<String, String>> replacePics = videoTemplateFinal.getReplaceImageUrls();
        List<Map<String, String>> imgPics = videoTemplateFinal.getImageUrls();
        if (replacePics == null || replacePics.isEmpty()) {
            if(imgPics == null || imgPics.isEmpty()) {
                body.put("pics_in_pic", new ArrayList<>());
            }else{
                body.put("pics_in_pic", picsInPic);
            }
        }else {
            List<Map<String, String>> picsConcat = new ArrayList<>();

            List<Map<String, String>> pics = videoTemplateFinal.getImageUrls();

            outerLoop:
            for (Map<String, String> pic : pics) {
                for (Map<String, String> replacePic : replacePics) {
                    if (pic.get("from").equals(replacePic.get("from")) && pic.get("to").equals(replacePic.get("to"))) {
                        //先判断有没有isShow="0"，如果是隐藏，则不能传这张图
                        if (replacePic.get("isShow") == null || "1".equals(replacePic.get("isShow"))){
                            picsConcat.add(replacePic); // 跳出两层循环，直接进入外层下一次迭代
                        }

                        continue outerLoop; // 跳出两层循环，直接进入外层下一次迭代
                    }
                }

                picsConcat.add(pic);
            }

            body.put("pics_in_pic", picsConcat);
        }

        body.put("output_media_config", outputUrl);
        body.put("subtitle_agent_res", subtitleAgentRes);

        //获取模板id
        VideoTemplateConfig videoTemplateConfig = videoTemplateConfigService.getById(videoTemplateFinal.getTemplateId());
        body.put("template_id", videoTemplateConfig.getLinkId());

        log.info("模板成片");

        HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
        ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class);
        Map<String, Object> respBody = response.getBody();
        if (respBody == null) {
            String msg = "模板成片接口响应为空";
            videoTemplateFinal.setStatus(7);
            videoTemplateFinal.setErrMsg(msg);
            videoTemplateFinalService.updateById(videoTemplateFinal);
            log.error("模板成片，模板返回内容为空，id={}", id);
            return "";
        }

        if (respBody != null && Boolean.TRUE.equals(respBody.get("success"))) {
            Map<String, Object> data = (Map<String, Object>) respBody.get("data");
            if (data != null && "submitted".equals(data.get("status"))) {
                String linkId = (String) data.get("job_id");

                //生成成功，则调用自动更新状态定时任务
                videoTemplateUpdateStatusTask.start(linkId);
                return linkId;
            } else {
                // TODO 这里如果失败了，直接在这一步更新status的状态。
                String msg = (String) respBody.get("message");
                videoTemplateFinal.setStatus(7);
                videoTemplateFinal.setErrMsg(msg);
                videoTemplateFinalService.updateById(videoTemplateFinal);
                log.error("模板成片STEP【调用合成模板视频接口】失败，id={}，错误信息为：{}", id, msg);

                return "";
            }
        } else {
            String msg = (String) respBody.get("message") == null ? "模板成片STEP【调用合成模板视频接口】返回数据格式无法解析" : (String) respBody.get("message");
            videoTemplateFinal.setStatus(7);
            videoTemplateFinal.setErrMsg(msg);
            videoTemplateFinalService.updateById(videoTemplateFinal);
            log.error("模板成片STEP【调用合成模板视频接口】失败，id={}，错误信息为：{}", id, msg);
            return "";
        }
    }

    /**
     * 模板视频预览
     * @param videoTemplateRequest 视频模板请求
     * @param userId 用户ID
     * @param merId 商户ID
     */
    @Override
    public Map<String, Object> videoTemplatePreview(VideoTemplateRequest videoTemplateRequest, Integer userId, Integer merId) {
        //首先对用户输入的内容进行曲emoji化
        String contentFilterEmoji = EmojiFilterUtil.removeEmojis(videoTemplateRequest.getContent());
        videoTemplateRequest.setContent(contentFilterEmoji);

        /**
         * 判断是编辑还是直接合成预览，如果是编辑的话，id需要从request中获取
         */
        Integer editId = videoTemplateRequest.getId();
        Integer id = 0;
        VideoTemplateFinal videoTemplateFinal;
        if (editId != null) {
            //编辑，需要将之前的记录置空
            updateVideoFinal(videoTemplateRequest);
            videoTemplateFinal = videoTemplateFinalService.getById(editId);
            id = editId;
        }else{
            //先写入表中，后续所有的操作都是对原记录的更新。
            videoTemplateFinal = insertVideoFinal(videoTemplateRequest, userId, merId);
            id = videoTemplateFinal.getId();

            //在这里更新每日任务进来的
            if ("weektask".equals(videoTemplateRequest.getSource())){
                //更新weektask那条记录的状态
                WeekTask weekTask = new WeekTask();
                weekTask.setId(videoTemplateRequest.getSourceId());
                weekTask.setTemplateId(id);
                weekTaskService.updateById(weekTask);
            }
        }

        Map<String, Object> map = new HashMap<>();

        Integer dpVoiceId = videoTemplateRequest.getDpVoiceId() == null ? 0 : videoTemplateRequest.getDpVoiceId();
        /**
         * STEP1: 先合成声音(未先点击合成视听)
         */
        if(videoTemplateRequest.getDpVoiceId() == null || videoTemplateRequest.getDpVoiceId() == 0){
            //先合成声音
            log.info("模板成片-STEP【合成音频】开始, 视频模板id: {}", id);
            Map<String, Object> params = createDpVoiceParams(videoTemplateRequest);

            Map<String, Object> dpVoiceRes = digitalPersonService.addDpVoice(params, userId, merId);
            if (dpVoiceRes.get("code").equals(0)){
//                DpVoice dpVoiceTemp = dpVoiceService.lambdaQuery()
//                        .eq(DpVoice::getLinkId, dpVoiceRes.get("voiceId"))
//                        .one();
//
//                dpVoiceId = dpVoiceTemp.getId();
                dpVoiceId = (Integer) dpVoiceRes.get("id");
            }else{
                // 合成音频失败，直接返回，并将数据库status更新为3
                String errMsg = dpVoiceRes.get("msg").toString();
                log.error("模板成片-STEP【合成音频】失败, id:{}, 失败原因：{}", id, errMsg);
                videoTemplateFinal.setStatus(3);

                //即使失败了也会有dpvoiceid
                dpVoiceId = (Integer) dpVoiceRes.get("id");
                videoTemplateFinal.setVoiceId(dpVoiceId);
                videoTemplateFinal.setErrMsg(errMsg);
                videoTemplateFinalService.updateById(videoTemplateFinal);

                map.put("msg", errMsg);
                return map;
            }
        }

        /**
         *
         * STEP2: 检查声音是否已经合成完毕
         */
        final Integer finalDpVoiceId = dpVoiceId;
        final Integer finalId = id;
        final AtomicInteger retryCount = new AtomicInteger(0);
        final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        final CompletableFuture<Void> completionFuture = new CompletableFuture<>();
        //同步阻塞检查声音合成
        ScheduledFuture<?> scheduledTask = scheduler.scheduleAtFixedRate(() -> {
            try {
                if (retryCount.get() >= 10) {
                    log.warn("模板成片-STEP【声音状态判断】声音合成检查超时，已重试10次，voiceId={}", finalDpVoiceId);
                    completionFuture.complete(null);
                    return;
                }
                
                DpVoice dpVoice = dpVoiceService.getById(finalDpVoiceId);

                //TODO 检查声音记录是否存在

                if (dpVoice != null && dpVoice.getStatus() == 9 && dpVoice.getOssUrl() != null && !dpVoice.getOssUrl().isEmpty()) {
                    log.info("模板成片-STEP【声音状态判断】声音合成完成，停止定时任务，id={}, voiceId={}", finalId, finalDpVoiceId);
                    completionFuture.complete(null);
                    return;
                }
                
                retryCount.incrementAndGet();
            } catch (Exception e) {
                log.error("检查声音合成状态异常: {}", e.getMessage(), e);
                completionFuture.completeExceptionally(e);
            }
        }, 30, 30, TimeUnit.SECONDS);
        
        // 当任务完成时关闭调度器
        completionFuture.whenComplete((result, throwable) -> {
            scheduledTask.cancel(false);
            scheduler.shutdown();
        });
        
        // 如果需要同步等待，可以使用以下代码（但会阻塞当前线程）
        completionFuture.join();

        //这里先更新下声音id
        videoTemplateFinal.setVoiceId(finalDpVoiceId);
        videoTemplateFinalService.updateById(videoTemplateFinal);

        /**
         * STEP3: 调用字幕agent
         */
        DpVoice dpVoice = dpVoiceService.getById(dpVoiceId);
        List<Map<String, Object>> voiceContent = dpVoice.getSubtitles();
        StringBuilder sb = new StringBuilder();
        //对蝉镜返回的字幕进行格式化
        for (Map<String, Object> item : voiceContent){
            // String objectName = String.format("%s/%d/%d/%s/%s", rootFolder, merId, userId, folderName, fileName);
            //[00:00 - 00:04]: 一盏离愁 孤单伫立在窗口
            String formatString = String.format("[%s - %s]: %s",item.get("start_time").toString(),item.get("end_time").toString(),item.get("subtitle").toString());
            sb.append(formatString);
        }

        String voiceContentString = sb.toString();

        //调用agent返回
        Map<String , Object> subtitleAgentApi =  videoSubtitleAgentApiNew(voiceContentString);
        String subtitleMsg = (String) subtitleAgentApi.get("msg");
        if (subtitleMsg != null) {
            log.error("模板成片-STEP【调用字幕agent】失败, id:{}, 失败原因：{}", id, subtitleMsg);
            videoTemplateFinal.setStatus(4);
            videoTemplateFinal.setErrMsg(subtitleMsg);
            videoTemplateFinalService.updateById(videoTemplateFinal);

            map.put("msg", subtitleMsg);
            return map;
        }

        //调用agent成功，扣点
        clientLoginService.updateMerPoint(userId,
                merId,
                "subtitleAgent",
                videoTemplateFinal.getId(),
                agentClientService.getMouleName("subtitleAgent"),
                "",
                agentClientService.getPointByDuration("subtitleAgent", null),
                null
        );

        //更新字段
        videoTemplateFinal.setSubtitleAgentApi(subtitleAgentApi);
        //获取标题字段
//        Map<String, Object> titleConent = (Map<String, Object>)subtitleAgentApi.get("title");
//        videoTemplateFinal.setTitle((String)titleConent.get("content"));

        Map<String, Object> titleConent = (Map<String, Object>)subtitleAgentApi.get("title");

        List<String> highlights = (List<String>)titleConent.get("highlight");
        String contentHighlights = (String)titleConent.get("content");
        String titleRes = FileUtil.processHighlightText(contentHighlights, highlights);

        videoTemplateFinal.setTitle(titleRes);

        //更新最终合成的视频时长
        videoTemplateFinal.setDuration(dpVoice.getDuration());

        videoTemplateFinalService.updateById(videoTemplateFinal);

        List<Map<String, Object>> picsKeywords = (List<Map<String, Object>>)subtitleAgentApi.get("pictureInPicture");

        /**
         * STEP4: 调用图片搜索
         */
//        List<Map<String, String>> pictureInPicture = videoPicAgentApi(picsKeywords);
//        if (pictureInPicture == null || pictureInPicture.size() == 0) {
//            log.error("模板成片-STEP【调用图片搜索】失败, id:{}", id);
//            String picErrMsg = "搜索素材图片失败";
//            videoTemplateFinal.setStatus(5);
//            videoTemplateFinal.setErrMsg(picErrMsg);
//            videoTemplateFinalService.updateById(videoTemplateFinal);
//
//            map.put("msg", picErrMsg);
//            return map;
//        }
//
//        //更新图片字段
//        videoTemplateFinal.setImageUrls(pictureInPicture);
//        videoTemplateFinalService.updateById(videoTemplateFinal);


        //这里同步搜索 图片改成异步搜索！！！
        final Integer finalPicId = id;
        CompletableFuture.supplyAsync(() -> picSearchApi(picsKeywords))
                .thenAccept(pictureInPicture -> {
                    if (pictureInPicture == null || pictureInPicture.size() == 0) {
                        log.info("模板成片-STEP【调用图片搜索】失败, id:{}", finalPicId);
//                        String picErrMsg = "搜索素材图片失败";
//                        videoTemplateFinal.setStatus(5);
//                        videoTemplateFinal.setErrMsg(picErrMsg);
                        videoTemplateFinal.setImageUrls(new ArrayList<>());
                        videoTemplateFinalService.updateById(videoTemplateFinal);
//
//                        map.put("msg", picErrMsg);
//                        return;
                    }

                    log.info("搜索图片id={}, 结果为：{}", finalPicId, pictureInPicture);
                    //更新图片字段
                    if (pictureInPicture != null && pictureInPicture.size() >0){
                        for (Map<String, String> picLoop : pictureInPicture){
                            String imgUrlLoop = picLoop.get("imgUrl");
                            String fileTypeLoop = FileUtil.getRealFileExtensionFromUrl(imgUrlLoop);
                            String fileNameloop = System.currentTimeMillis() + "." + fileTypeLoop;
                            String ossUrlLoop = OssUtil.uploadFromUrlToOss(imgUrlLoop, fileNameloop, userId, merId, "image", "keywordimg", "noasset");
                            picLoop.put("imgUrl", ossUrlLoop);
                        }

                        //图片搜索成功，扣点
                        clientLoginService.updateMerPoint(userId,
                                merId,
                                "picSearch",
                                finalPicId,
                                agentClientService.getMouleName("picSearch"),
                                "",
                                agentClientService.getPointByDuration("picSearch", videoTemplateFinal.getDuration()),
                                videoTemplateFinal.getDuration()
                        );

                        videoTemplateFinal.setImageUrls(pictureInPicture);
                        videoTemplateFinalService.updateById(videoTemplateFinal);
                    }
                })
                .exceptionally(ex -> {
                    log.info("模板成片-STEP【调用图片搜索】异常, id:{}, 错误信息:{}", finalPicId, ex.getMessage());
//                    String picErrMsg = "搜索素材图片异常: " + ex.getMessage();
//                    videoTemplateFinal.setStatus(5);
//                    videoTemplateFinal.setErrMsg(picErrMsg);
//                    videoTemplateFinalService.updateById(videoTemplateFinal);
//
//                    map.put("msg", picErrMsg);
                    return null;
                });

        //将记录的状态改为已经预览完成
        videoTemplateFinal.setStatus(8);
        videoTemplateFinalService.updateById(videoTemplateFinal);

        HashMap<String, Object> result = new HashMap<>();
        result.put("subtitleAgentApi", videoTemplateFinal.getSubtitleAgentApi());
//        result.put("imageUrls", videoTemplateFinal.getImageUrls());
        result.put("id", id);
        return result;
    }


    /**
     * 最终生成模板视频入口
     * @param videoTemplateRequest 视频模板请求
     * @param userId 用户ID
     * @param merId 商户ID
     */
    @Override
    public VideoTemplateFinal videoTemplateCommon(VideoTemplateRequest videoTemplateRequest, Integer userId, Integer merId) {
        //首先对用户输入的内容进行曲emoji化
        String contentFilterEmoji = EmojiFilterUtil.removeEmojis(videoTemplateRequest.getContent());
        videoTemplateRequest.setContent(contentFilterEmoji);

        // 生成输出URL
        String fileName = System.currentTimeMillis() + ".mp4";
        String outputUrl = OssUtil.getOssUrl("noasset", "videotemplate", userId, merId, fileName);

        //合成声音的蝉镜id,预览-通过dpVoiceId获取，最终合成-先合成音频接口返回获取
        //String dpVoiceLinkId;

        //合成声音的id，如果是先合成的，那就从参数中获得
        Integer dpVoiceId=0;
        Integer finalId=0;

        //如果是直接点最终合成，需要先合成音频->调用字幕agent->搜索图片(同步) & 合成数字人视频 -》模板视频合成
        Integer id = videoTemplateRequest.getId();
        if(id == null){
            //先插入一条模板成片的记录到表中,后续的操作都是更新这条记录的字段。
            /**
             * STEP0: 先生成视频模板表的记录
             */
            VideoTemplateFinal videoTemplateFinal = insertVideoFinal(videoTemplateRequest, userId, merId);
            finalId = videoTemplateFinal.getId();

            /**
             * STEP1: 先合成声音(未先点击合成视听)
             */
            // 如果没有合成声音的id，需要先生成
            if (videoTemplateRequest.getDpVoiceId() == null) {
                //如果在这个过程中，没有传递voiceId, 则先合成音频，定时任务判断音频生成成功且返回字幕文件才去调用字幕agent
                Map<String, Object> params = createDpVoiceParams(videoTemplateRequest);

                Map<String, Object> dpVoiceRes = digitalPersonService.addDpVoice(params, userId, merId);
                if (dpVoiceRes.get("code").equals(0)){
//                    DpVoice dpVoiceTemp = dpVoiceService.lambdaQuery()
//                            .eq(DpVoice::getLinkId, dpVoiceRes.get("voiceId"))
//                            .one();
//
//                    dpVoiceId = dpVoiceTemp.getId();
                    dpVoiceId = (Integer) dpVoiceRes.get("id");
                }else{
                    // 合成音频失败，直接返回，并将数据库status更新为3
                    String errMsg = dpVoiceRes.get("msg").toString();
                    log.error("模板成片-STEP【合成音频】失败, id:{}, 失败原因：{}", id, errMsg);
                    videoTemplateFinal.setStatus(3);

                    //即使失败了也会有dpvoiceid
                    dpVoiceId = (Integer) dpVoiceRes.get("id");
                    videoTemplateFinal.setVoiceId(dpVoiceId);
                    videoTemplateFinal.setErrMsg(errMsg);
                    videoTemplateFinalService.updateById(videoTemplateFinal);

                    //推送ws事件
                    String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                    pushWsEvent(finalId, userId, 3, eventName);

                    return null;
                }

            }else {
                dpVoiceId = videoTemplateRequest.getDpVoiceId();
            }

            //这里先更新下声音id
            videoTemplateFinal.setVoiceId(dpVoiceId);

            videoTemplateFinalService.updateById(videoTemplateFinal);

            /**
             *
             * STEP2: 检查声音是否已经合成完毕
             */
            //定时任务，合成声音有结果 以后才去进行下面的步骤。定时任务中，会先调用字幕agent返回字幕信息和图片关键字，然后调用搜索图片接口。
            CompletableFuture<Map<String, Object>> dpVoiceStatusFutureRes = scheduleVoiceStatusCheck(dpVoiceId, finalId, videoTemplateRequest);
//            Boolean dpVoiceStatusFutureRes = scheduleVoiceStatusCheck1(dpVoiceId, finalId, videoTemplateRequest);
            dpVoiceStatusFutureRes.thenAccept(voiceRes -> {
                // 这里合成视频-》视频模板agent调用就是通用的了。
                log.info("模板成片-合成音频结束{}", voiceRes);
            });

        }else{

            dpVoiceId = videoTemplateRequest.getDpVoiceId();
        }

        /**
         * STEP4: 合成视频
         */
        // 无论有没有id，都要先合成数字人视频，这里先合成视频，然后等待结果。
        //String type, String voiceId, Integer userId, Integer merId
        try{
            Integer paramId = id == null ? finalId : id;
            log.info("最终合成-STEP【合成视频】开始，id={}", paramId);

            String type = videoTemplateRequest.getVideoType();
            //根据voiceId拿到linkId
            DpVoice dpVoice = dpVoiceService.getById(dpVoiceId);
            String dpVoiceLinkId = dpVoice.getLinkId();

            //在这里拿到前面步骤存入表中的画中画素材、字幕agent返回字段
            //由于可能是先点预览，再点最终合成，所以带了id，这里要重新拿一次视频模板数据。
            VideoTemplateFinal byId = videoTemplateFinalService.getById(paramId);

            //在这里更新每日任务进来的
            if ("weektask".equals(videoTemplateRequest.getSource())){
                //更新weektask那条记录的状态
                WeekTask weekTask = new WeekTask();
                weekTask.setId(videoTemplateRequest.getSourceId());
                weekTask.setTemplateId(paramId);
                weekTaskService.updateById(weekTask);
            }

            //如果是先预览，再合成，那就要更新状态为生成中
            if(id != null && id > 0){
                byId.setStatus(1);
                videoTemplateFinalService.updateById(byId);
            }

            // 合成数字人视频
            Map<String, Object> params = createDpVideoParams(videoTemplateRequest);

            Map<String, Object> subtitleAgentVideo = byId.getSubtitleAgentApi();
            String dpVideoName = "未命名标题(仅数字人)";

            if (subtitleAgentVideo != null && !subtitleAgentVideo.isEmpty()) {
                Map<String, Object> titleVideo = (Map<String, Object>)subtitleAgentVideo.get("title");
                if (titleVideo != null) {
                    dpVideoName = (String) titleVideo.get("content") + "(仅数字人)";
                    log.info("生成的数字人视频标题为：{}", dpVideoName);
                }
            }

            Map<String, Object> sourceMap = new HashMap<>();
            sourceMap.put("sourceName", "template");
            sourceMap.put("sourceId", paramId);

            DgVideo dgVideoTemp = digitalPersonService.insertDpVideo(userId, merId, dpVideoName, sourceMap, videoTemplateRequest, dpVoiceId);
            CompletableFuture<Integer> dpVideoFuture = digitalPersonService.addDpVideoCommon(dgVideoTemp,params, "audio", videoTemplateRequest.getAudioManLinkId(), videoTemplateRequest.getContent(), dpVoiceLinkId, userId, merId, dpVideoName, null);

            dpVideoFuture.thenAccept(dpVideoId -> {
                // 检查合成是否成功
                if (dpVideoId == null || dpVideoId == 0) {
                    throw new CrmebException("数字人视频合成失败，未返回有效ID");
                }

                //先更新数字人视频id
                byId.setDpId(dpVideoId);
                videoTemplateFinalService.updateById(byId);

                final int[] retryCount = {0};
                final int[] retryPicCount = {0};
                Runnable checkTask = new Runnable() {
                    @Override
                    public void run() {
                        DgVideo dpVideo = dgVideoService.getById(dpVideoId);
                        VideoTemplateFinal finalResById = videoTemplateFinalService.getById(paramId);

                        if (dpVideo != null) {
                            if (dpVideo.getStatus() == 30) {
                                if (finalResById.getSubtitleAgentApi() == null || finalResById.getImageUrls() == null) {
                                    if(retryPicCount[0] < 3){
                                        log.info("模板成片-生成视频成功，图片搜索未成功，id={}", paramId);
                                        retryPicCount[0]++;
                                        taskScheduler.schedule(this, new Date(System.currentTimeMillis() + 60 * 1000));
                                        return;
                                    }
                                }

                                if (dpVideo.getOssVideoUrl() == null) {
                                    log.info("模板成片-视频ossUrl地址未完成, id={}", paramId);
                                    taskScheduler.schedule(this, new Date(System.currentTimeMillis() + 60 * 1000));
                                    return; // 退出当前方法，等待下一次执行
                                }

                                // 更新合成数字人视频的标题
                                Map<String, Object> subtitleAgentVideo2 = finalResById.getSubtitleAgentApi();
                                String dpVideoName2 = "未命名标题(仅数字人)";

                                if (subtitleAgentVideo2 != null && !subtitleAgentVideo2.isEmpty()) {
                                    Map<String, Object> titleVideo2 = (Map<String, Object>)subtitleAgentVideo2.get("title");
                                    if (titleVideo2 != null) {
                                        dpVideoName2 = (String) titleVideo2.get("content") + "(仅数字人)";
                                        log.info("更新生成的数字人视频标题为：{}", dpVideoName2);
                                    }
                                }
                                dpVideo.setName(dpVideoName2);
                                dgVideoService.updateById(dpVideo);

                                log.info("生成视频成功，开始进行模板视频的合成");
                                /**
                                 * STEP5: 调用模板成片的api
                                 */
                                if (finalResById.getStatus() != 4 && finalResById.getStatus() != 6){
                                    String templateResId = videoTemplateAgentApi(paramId, dpVideoId, finalResById.getImageUrls(), userId, merId, finalResById.getSubtitleAgentApi(), outputUrl);

                                    //更新视频模板信息，数字人视频、生成的OSS地址
                                    finalResById.setLinkId(templateResId);
                                    finalResById.setDpId(dpVideoId);
                                    finalResById.setOssUrl(outputUrl);

                                    videoTemplateFinalService.updateById(finalResById);
                                    return;
                                }
                            } else if (dpVideo.getStatus() == 10 && retryCount[0] < 10) {
                                retryCount[0]++;
                                log.info("模板视频-STEP【合成数字人视频】生成中，{}分钟后重试，id={}, dpVideoId={}, retry={}", retryCount[0], paramId, dpVideoId, retryCount[0]);
                                taskScheduler.schedule(this, new Date(System.currentTimeMillis() + 60 * 1000));
                            } else if (dpVideo.getStatus() != 10 && dpVideo.getStatus() != 30) {
                                //合成数字人视频失败
//                                finalResById.setStatus(6);
//                                finalResById.setErrMsg(dpVideo.getMsg());
////                                finalResById.setId(paramId);
//                                videoTemplateFinalService.updateById(finalResById);
                                VideoTemplateFinal videoTemplateFinalFail = new VideoTemplateFinal();
                                videoTemplateFinalFail.setStatus(6);
                                videoTemplateFinalFail.setId(paramId);
                                videoTemplateFinalFail.setErrMsg(dpVideo.getMsg());
//
                                videoTemplateFinalService.updateById(videoTemplateFinalFail);
                                log.error("模板视频-STEP【合成数字人视频】失败，id={},dpId={}", paramId,dpVideoId);

                                //推送ws事件
                                String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                                pushWsEvent(paramId, userId, 6, eventName);

                                return;
                            }
                        } else {
                            //合成数字人视频失败
                            finalResById.setStatus(6);
                            finalResById.setId(paramId);
                            finalResById.setErrMsg("未找到数字人视频");
                            videoTemplateFinalService.updateById(finalResById);
                            log.error("模板视频-STEP【合成数字人视频】失败，未找到视频记录，id={},dpId={}", paramId,dpVideoId);

                            //推送ws事件
                            String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                            pushWsEvent(paramId, userId, 6, eventName);

                            return;
                        }
                    }
                };
                // 首次1分钟后执行
                taskScheduler.schedule(checkTask, new Date(System.currentTimeMillis() + 60 * 1000));
            }).exceptionally(ex -> {
                // 处理异常
                log.error("合成数字人视频时发生异常: {}", ex.getMessage());
                return null;
            });

            return byId;
        } catch (Exception e) {
            throw new CrmebException(e.getMessage());
        }
    }

    /**
     * 模板视频编辑入口
     * @param videoTemplateRequest 视频模板请求
     * @param userId 用户ID
     * @param merId 商户ID
     */
    @Override
    public void videoTemplateEdit(VideoTemplateRequest videoTemplateRequest, Integer userId, Integer merId) {
        //首先对用户输入的内容进行曲emoji化
        String contentFilterEmoji = EmojiFilterUtil.removeEmojis(videoTemplateRequest.getContent());
        videoTemplateRequest.setContent(contentFilterEmoji);

        // 生成输出URL
        String fileName = System.currentTimeMillis() + ".mp4";
        String outputUrl = OssUtil.getOssUrl("noasset", "videotemplate", userId, merId, fileName);

        //获取模板视频的记录
        Integer id = videoTemplateRequest.getId();
        VideoTemplateFinal videoTemplateFinalOld = videoTemplateFinalService.getById(id);

        String contentRecord = videoTemplateFinalOld.getContent();
        Integer dpVoiceIdRecord = videoTemplateFinalOld.getVoiceId();
        String dpManLinkIdRecord = videoTemplateFinalOld.getDpManId();
        String audioManLinkIdRecord = videoTemplateFinalOld.getAudioManId();

        //传过来的dpVoice
        Integer dpVoiceIdRequest = videoTemplateRequest.getDpVoiceId();
        String contentRequest = videoTemplateRequest.getContent();
        String title = videoTemplateRequest.getTitle();
        String audioManLinkIdRequest = videoTemplateRequest.getAudioManLinkId();
        String dpManLinkIdRequest = videoTemplateRequest.getDpManLinkId();

        //要先清空之前的字段，更新成传入的字段
        VideoTemplateFinal updatevideoTemplateFinal = new VideoTemplateFinal();
//        videoTemplateFinal.setContent(contentRequest);
//        videoTemplateFinal.setTitle("ceshi");
//        videoTemplateFinal.setDpId(null);
//        videoTemplateFinal.setVoiceId(null);
//        videoTemplateFinal.setSubtitleAgentApi(null);
//        videoTemplateFinal.setImageUrls(null);
//        videoTemplateFinal.setErrMsg(null);
//        videoTemplateFinal.setStatus(1);
////        updatevideoTemplateFinal.setId(id);

        UpdateWrapper<VideoTemplateFinal> updateWrapper = new UpdateWrapper<>();
        updateWrapper.eq("id", id);
        updateWrapper.set("content", contentRequest);
        updateWrapper.set("title", title);
        updateWrapper.set("dp_id", null);
        updateWrapper.set("voice_id", null);
        updateWrapper.set("subtitle_agent_api", null);
        updateWrapper.set("image_urls", null);
        updateWrapper.set("err_msg", null);
        updateWrapper.set("status", 1);
        videoTemplateFinalService.update(null, updateWrapper);

//        videoTemplateFinalService.updateById(videoTemplateFinal);

        //新的dpVoiceId，下面步骤使用
        Integer dpVoiceIdNew = 0;
        VideoTemplateFinal videoTemplateFinal = videoTemplateFinalService.getById(id);

        /**
         * STEP1: 先合成声音(未先点击合成视听)
         * 如果content的传入的内容和记录中的content不同，或者dpvoiceId不同，则表示需要重新生成声音记录
         */
        //如果这两个不同 ，那肯定要重新生成音频
        if (!contentRequest.equals(contentRecord) || !audioManLinkIdRequest.equals(audioManLinkIdRecord)){
            //如果合成试听的id 和 记录的一样，则需重新生成，表示是tts，如果不同则需要重新生成
            if (dpVoiceIdRequest.equals(dpVoiceIdRecord)){
                //重新生成音频
                Map<String, Object> params = createDpVoiceParams(videoTemplateRequest);
                Map<String, Object> dpVoiceRes = digitalPersonService.addDpVoice(params, userId, merId);
                if (dpVoiceRes.get("code").equals(0)){
                    dpVoiceIdNew = (Integer) dpVoiceRes.get("id");
                }else{
                    // 合成音频失败，直接返回，并将数据库status更新为3
                    String errMsg = dpVoiceRes.get("msg").toString();
                    log.error("模板成片编辑-STEP【合成音频】失败, id:{}, 失败原因：{}", id, errMsg);
                    videoTemplateFinal.setStatus(3);

                    //即使失败了也会有dpvoiceid
                    dpVoiceIdNew = (Integer) dpVoiceRes.get("id");
                    videoTemplateFinal.setVoiceId(dpVoiceIdNew);
                    videoTemplateFinal.setErrMsg(errMsg);
                    videoTemplateFinalService.updateById(videoTemplateFinal);

                    //推送ws事件
                    String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                    pushWsEvent(id, userId, 3, eventName);

                    return;
                }
            }else{
                dpVoiceIdNew = dpVoiceIdRequest;
            }
        }else {
            //如果内容一样，音色一样，数字人一样，直接将
            dpVoiceIdNew = dpVoiceIdRequest;
        }

        //这里先更新下声音id
        videoTemplateFinal.setVoiceId(dpVoiceIdNew);
        videoTemplateFinalService.updateById(videoTemplateFinal);

        /**
         *
         * STEP2: 检查声音是否已经合成完毕
         */
        //定时任务，合成声音有结果 以后才去进行下面的步骤。定时任务中，会先调用字幕agent返回字幕信息和图片关键字，然后调用搜索图片接口。
        CompletableFuture<Map<String, Object>> dpVoiceStatusFutureRes = scheduleVoiceStatusCheck(dpVoiceIdNew, id, videoTemplateRequest);
        dpVoiceStatusFutureRes.thenAccept(voiceRes -> {
            // 这里合成视频-》视频模板agent调用就是通用的了。
            log.info("模板成片-合成音频结束{}", voiceRes);
        });

        /**
         * STEP4: 合成视频
         */
        // 无论有没有id，都要先合成数字人视频，这里先合成视频，然后等待结果。
        //String type, String voiceId, Integer userId, Integer merId
        try{
            log.info("最终合成-STEP【合成视频】开始，id={}", id);

            String type = videoTemplateRequest.getVideoType();
            //根据voiceId拿到linkId
            DpVoice dpVoice = dpVoiceService.getById(dpVoiceIdNew);
            String dpVoiceLinkId = dpVoice.getLinkId();

            //在这里拿到前面步骤存入表中的画中画素材、字幕agent返回字段
            //由于可能是先点预览，再点最终合成，所以带了id，这里要重新拿一次视频模板数据。
            VideoTemplateFinal byId = videoTemplateFinalService.getById(id);

            // 合成数字人视频
            Map<String, Object> params = createDpVideoParams(videoTemplateRequest);

            Map<String, Object> subtitleAgentVideo = byId.getSubtitleAgentApi();
            String dpVideoName = "未命名标题(仅数字人)";

            if (subtitleAgentVideo != null && !subtitleAgentVideo.isEmpty()) {
                Map<String, Object> titleVideo = (Map<String, Object>)subtitleAgentVideo.get("title");
                if (titleVideo != null) {
                    dpVideoName = (String) titleVideo.get("content") + "(仅数字人)";
                    log.info("生成的数字人视频标题为：{}", dpVideoName);
                }
            }

            Map<String, Object> sourceMap = new HashMap<>();
            sourceMap.put("sourceName", "template");
            sourceMap.put("sourceId", id);

            DgVideo dgVideoTemp2 = digitalPersonService.insertDpVideo(userId, merId, dpVideoName, sourceMap, videoTemplateRequest, dpVoiceIdNew);
            CompletableFuture<Integer> dpVideoFuture = digitalPersonService.addDpVideoCommon(dgVideoTemp2,params, "audio", videoTemplateRequest.getAudioManLinkId(), videoTemplateRequest.getContent(), dpVoiceLinkId, userId, merId, dpVideoName, null);

            dpVideoFuture.thenAccept(dpVideoId -> {
                // 检查合成是否成功
                if (dpVideoId == null || dpVideoId == 0) {
                    throw new CrmebException("数字人视频合成失败，未返回有效ID");
                }

                //先更新数字人视频id
                byId.setDpId(dpVideoId);
                videoTemplateFinalService.updateById(byId);

                final int[] retryCount = {0};
                final int[] retryPicCount = {0};
                Runnable checkTask = new Runnable() {
                    @Override
                    public void run() {
                        DgVideo dpVideo = dgVideoService.getById(dpVideoId);
                        VideoTemplateFinal finalResById = videoTemplateFinalService.getById(id);

                        if (dpVideo != null) {
                            if (dpVideo.getStatus() == 30) {
                                if (finalResById.getSubtitleAgentApi() == null || finalResById.getImageUrls() == null) {
                                    if (retryPicCount[0] < 3){
                                        log.info("模板成片-生成视频成功，图片搜索未成功，id={}", id);
                                        retryPicCount[0]++;
                                        taskScheduler.schedule(this, new Date(System.currentTimeMillis() + 60 * 1000));
                                        return; // 退出当前方法，等待下一次执行
                                    }
                                }

                                if (dpVideo.getOssVideoUrl() == null) {
                                    log.info("模板成片-视频ossUrl地址未完成, id={}", id);
                                    taskScheduler.schedule(this, new Date(System.currentTimeMillis() + 60 * 1000));
                                    return; // 退出当前方法，等待下一次执行
                                }

                                log.info("生成视频成功，开始进行模板视频的合成");
                                /**
                                 * STEP5: 调用模板成片的api
                                 */
                                if (finalResById.getStatus() != 4 && finalResById.getStatus() != 6){
                                    String templateResId = videoTemplateAgentApi(id, dpVideoId, finalResById.getImageUrls(), userId, merId, finalResById.getSubtitleAgentApi(), outputUrl);

                                    //更新视频模板信息，数字人视频、生成的OSS地址
                                    finalResById.setLinkId(templateResId);
                                    finalResById.setDpId(dpVideoId);
                                    finalResById.setOssUrl(outputUrl);

                                    videoTemplateFinalService.updateById(finalResById);
                                    return;
                                }

                            } else if (dpVideo.getStatus() == 10 && retryCount[0] < 6) {
                                retryCount[0]++;
                                log.info("模板视频-STEP【合成数字人视频】生成中，{}分钟后重试，id={}, dpVideoId={}, retry={}", retryCount[0], id, dpVideoId, retryCount[0]);
                                taskScheduler.schedule(this, new Date(System.currentTimeMillis() + 60 * 1000));
                            } else if (dpVideo.getStatus() != 10 && dpVideo.getStatus() != 30) {
                                //合成数字人视频失败
//                                finalResById.setStatus(6);
//                                finalResById.setErrMsg(dpVideo.getMsg());
//                                videoTemplateFinalService.updateById(finalResById);
                                VideoTemplateFinal videoTemplateFinalFail = new VideoTemplateFinal();
                                videoTemplateFinalFail.setId(id);
                                videoTemplateFinalFail.setStatus(6);
                                videoTemplateFinalFail.setErrMsg(dpVideo.getMsg());
                                videoTemplateFinalService.updateById(videoTemplateFinalFail);
                                log.error("模板视频-STEP【合成数字人视频】失败，id={},dpId={}", id,dpVideoId);

                                //推送ws事件
                                String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                                pushWsEvent(id, userId, 6, eventName);

                                return;
                            }
                        } else {
                            //合成数字人视频失败
                            finalResById.setStatus(6);
                            finalResById.setErrMsg("未找到数字人视频");
                            videoTemplateFinalService.updateById(finalResById);
                            log.error("模板视频-STEP【合成数字人视频】失败，未找到视频记录，id={},dpId={}", id,dpVideoId);

                            //推送ws事件
                            String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                            pushWsEvent(id, userId, 6, eventName);

                            return;
                        }
                    }
                };
                // 首次1分钟后执行
                taskScheduler.schedule(checkTask, new Date(System.currentTimeMillis() + 60 * 1000));
            }).exceptionally(ex -> {
                // 处理异常
                log.error("合成数字人视频时发生异常: {}", ex.getMessage());
                return null;
            });
        } catch (Exception e) {
            throw new CrmebException(e.getMessage());
        }
    }

    /**
     * 素材替换
     * @param replacePicRequest
     * @return
     */
    @Override
    public Boolean replacePic(ReplacePicRequest replacePicRequest){
        VideoTemplateFinal videoTemplateFinal = videoTemplateFinalService.getById(replacePicRequest.getId());
        List<Map<String, String>> imgUrls = videoTemplateFinal.getImageUrls();
        List<Map<String, String>> replaceImgUrls = videoTemplateFinal.getReplaceImageUrls();

        Map<String, String> imgUrl = new HashMap<>();

        String from = replacePicRequest.getFrom();
        String to = replacePicRequest.getTo();
        String keyword = replacePicRequest.getKeyword();
        String singleImgUrl = replacePicRequest.getImgUrl();

        //是否显示图片，如果是替换素材，则isShow="1"
        String isShow = replacePicRequest.getIsShow() == null ? "1" : replacePicRequest.getIsShow();

        if (replaceImgUrls != null && replaceImgUrls.size() > 0) {
            //如果替换素材中已经有值
            Boolean isReplace = false;

            //之前是否已经在这个关键词替换过
            for (Map<String, String> map : replaceImgUrls){
                if (map.get("from").equals(from) && map.get("to").equals(to)){
                    map.put("imgUrl", singleImgUrl);
                    map.put("isShow", isShow);
                    isReplace = true;
                }
            }

            //如果之前替换的素材中没有这个关键词的，需要增加进去
            if(!isReplace){
                imgUrl.put("from", from);
                imgUrl.put("to", to);
                imgUrl.put("keyword", keyword);
                imgUrl.put("imgUrl", singleImgUrl);
                imgUrl.put("isShow", isShow);
                replaceImgUrls.add(imgUrl);
            }
        }else{
            //replaceImgUrls 为空
            imgUrl.put("from", from);
            imgUrl.put("to", to);
            imgUrl.put("keyword", keyword);
            imgUrl.put("imgUrl", singleImgUrl);
            imgUrl.put("isShow", isShow);

            List<Map<String, String>> arrayList = new ArrayList<>();
            arrayList.add(imgUrl);
            replaceImgUrls = arrayList;
        }

        videoTemplateFinal.setReplaceImageUrls(replaceImgUrls);
        return videoTemplateFinalService.updateById(videoTemplateFinal);
    }

    /**
     * 根据参数插入视频模板表
     * @return
     */
    private VideoTemplateFinal insertVideoFinal(VideoTemplateRequest videoTemplateRequest,Integer userId, Integer merId) {
        VideoTemplateFinal videoTemplateFinal = new VideoTemplateFinal();
        videoTemplateFinal.setTemplateId(videoTemplateRequest.getTemplateId());//模板id
        videoTemplateFinal.setUserId(userId); //用户id
        videoTemplateFinal.setMerId(merId); //商户id
        videoTemplateFinal.setIsFolder(0); //是否文件夹
        videoTemplateFinal.setContent(videoTemplateRequest.getContent());//存入原始内容
        videoTemplateFinal.setCreateTime(new Date());
        videoTemplateFinal.setAudioManId(videoTemplateRequest.getAudioManLinkId());//音色id
        videoTemplateFinal.setDpManId(videoTemplateRequest.getDpManLinkId());//数字人id
        videoTemplateFinal.setIsCommonDp(videoTemplateRequest.getIsCommonDp());//数字人id

        //插入数字人形象预览图
        Integer isCommonDp = videoTemplateRequest.getIsCommonDp();
        if (isCommonDp == 1) {
            PublicDigitalPerson publicDigitalPerson = publicDigitalPersonService.lambdaQuery()
                    .eq(PublicDigitalPerson::getLinkId, videoTemplateRequest.getDpManLinkId())
                    .one();

            if(publicDigitalPerson != null) {
                String figures = publicDigitalPerson.getFigure();
                List<Map<String, Object>> figureList = null;
                ObjectMapper objectMapper = new ObjectMapper();
                try {
                    figureList = objectMapper.readValue(figures, new TypeReference<List<Map<String, Object>>>() {});
                } catch (Exception e) {
                    log.error("解析figures字符串失败: {}", e.getMessage(), e);
                }

                for (Map<String, Object> figure : figureList) {
                    if ("sit_body".equals(figure.get("type"))){
                        String dpPreviewVideoUrl = (String)figure.get("preview_video_url");
                        videoTemplateFinal.setDpPreviewUrl(dpPreviewVideoUrl);
                        break;
                    }
                }
            }
        }else{
            CustomDigitalPerson customDigitalPerson = customDigitalPersonService.lambdaQuery()
                    .eq(CustomDigitalPerson::getLinkId, videoTemplateRequest.getDpManLinkId())
                    .one();

            if(customDigitalPerson != null) {
                videoTemplateFinal.setDpPreviewUrl(customDigitalPerson.getOssPreviewUrl());
            }
        }

        HashMap<String, Object> saveLocationMap = new HashMap<>();
        saveLocationMap.put("module", videoTemplateRequest.getSaveModule());
        saveLocationMap.put("parent_id", videoTemplateRequest.getSaveParentId());

        videoTemplateFinal.setSaveLocation(saveLocationMap);

        Integer parentId = videoTemplateRequest.getParentId() == null ? 0 : videoTemplateRequest.getParentId();
        videoTemplateFinal.setParentId(videoTemplateRequest.getParentId());//上级目录

        if (parentId == null || parentId == 0){
            videoTemplateFinal.setCompleteFolder("/");
        }else {
            VideoTemplateFinal parent = videoTemplateFinalService.getById(parentId);
            videoTemplateFinal.setCompleteFolder(parent.getCompleteFolder()+parentId+"/");
        }

        videoTemplateFinalService.save(videoTemplateFinal);

        return videoTemplateFinal;
    }


    /**
     * 模板编辑，将相应参数置为null
     * @return
     */
    private void updateVideoFinal(VideoTemplateRequest videoTemplateRequest) {
//        UpdateWrapper<VideoTemplateFinal> updateWrapper = new UpdateWrapper<>();
//        updateWrapper.eq("id", videoTemplateRequest.getId());
//        updateWrapper.set("content", videoTemplateRequest.getContent());
//        updateWrapper.set("title", null);
//        updateWrapper.set("dp_id", null);
//        updateWrapper.set("voice_id", null);
//        updateWrapper.set("subtitle_agent_api", null);
//        updateWrapper.set("image_urls", null);
//        updateWrapper.set("replace_image_urls", null);
//        updateWrapper.set("err_msg", null);
//        updateWrapper.set("link_id", null);
//        updateWrapper.set("dp_man_id", null);
//        updateWrapper.set("audio_man_id", null);
//        updateWrapper.set("status", 1);
//
//        Map<String, Object> saveLocationMap = new HashMap<>();
//        saveLocationMap.put("module", videoTemplateRequest.getSaveModule());
//        saveLocationMap.put("parent_id", videoTemplateRequest.getSaveParentId());
//
//        updateWrapper.set("save_location", saveLocationMap);
//
//        videoTemplateFinalService.update(null, updateWrapper);

        VideoTemplateFinal videoTemplateFinal = new VideoTemplateFinal();
        videoTemplateFinal.setId(videoTemplateRequest.getId());
        videoTemplateFinal.setContent(videoTemplateRequest.getContent());
        videoTemplateFinal.setTitle(null);
        videoTemplateFinal.setDpId(null);
        videoTemplateFinal.setVoiceId(null);
        videoTemplateFinal.setSubtitleAgentApi(null);
        videoTemplateFinal.setImageUrls(null);
        videoTemplateFinal.setReplaceImageUrls(null);
        videoTemplateFinal.setErrMsg(null);
        videoTemplateFinal.setLinkId(null);
        videoTemplateFinal.setDpId(null);
        videoTemplateFinal.setAudioManId(null);
        videoTemplateFinal.setStatus(1);

        videoTemplateFinal.setIsCommonDp(videoTemplateRequest.getIsCommonDp());//数字人id

        //插入数字人形象预览图
        Integer isCommonDp = videoTemplateRequest.getIsCommonDp();
        if (isCommonDp == 1) {
            PublicDigitalPerson publicDigitalPerson = publicDigitalPersonService.lambdaQuery()
                    .eq(PublicDigitalPerson::getLinkId, videoTemplateRequest.getDpManLinkId())
                    .one();

            if(publicDigitalPerson != null) {
                String figures = publicDigitalPerson.getFigure();
                List<Map<String, Object>> figureList = null;
                ObjectMapper objectMapper = new ObjectMapper();
                try {
                    figureList = objectMapper.readValue(figures, new TypeReference<List<Map<String, Object>>>() {});
                } catch (Exception e) {
                    log.error("解析figures字符串失败: {}", e.getMessage(), e);
                }

                for (Map<String, Object> figure : figureList) {
                    if ("sit_body".equals(figure.get("type"))){
                        String dpPreviewVideoUrl = (String)figure.get("preview_video_url");
                        videoTemplateFinal.setDpPreviewUrl(dpPreviewVideoUrl);
                        break;
                    }
                }
            }
        }else{
            CustomDigitalPerson customDigitalPerson = customDigitalPersonService.lambdaQuery()
                    .eq(CustomDigitalPerson::getLinkId, videoTemplateRequest.getDpManLinkId())
                    .one();

            if(customDigitalPerson != null) {
                videoTemplateFinal.setDpPreviewUrl(customDigitalPerson.getOssPreviewUrl());
            }
        }

        Map<String, Object> saveLocationMap = new HashMap<>();
        saveLocationMap.put("module", videoTemplateRequest.getSaveModule());
        saveLocationMap.put("parent_id", videoTemplateRequest.getSaveParentId());
        videoTemplateFinal.setSaveLocation(saveLocationMap);

        videoTemplateFinalService.updateById(videoTemplateFinal);


    }

    /**
     * 根据参数插入视频模板表
     * @return
     */
    @Override
    public void pushWsEvent(Integer id, Integer userId, Integer status, String eventName) {
        Map<String, Object> data = new HashMap<>();
        data.put("id", id);
//        data.put("linkId", linkId);
        data.put("status", status);

        Map<String, Object> message = new HashMap<>();
        message.put("event", eventName);
        message.put("data", data);

        String jsonMessage = JSON.toJSONString(message);
        webSocketSessionManager.sendMessageToUser(userId, jsonMessage);
        log.info("向用户 {} 推送视频模板生成状态更新: {}", userId, jsonMessage);
    }

    /**
     * 先合成数字人视频参数
     * @return
     */
    private Map<String, Object> createDpVideoParams(VideoTemplateRequest videoTemplateRequest) {
        String type = videoTemplateRequest.getVideoType();
        //拼接params
        // 构建请求参数
        Map<String, Object> params = new HashMap<>();

        //增加背景颜色
        params.put("bg_color", "#F0F0F0");

        // 构建person对象
        Map<String, Object> person = new HashMap<>();
        person.put("id", videoTemplateRequest.getDpManLinkId()); // linkId
        person.put("x", 0);

        Integer width = 1080;
        Integer height = 1920;

        person.put("width", width);
//        person.put("height", height);

        //公式计算y的值，合成视频时用了1080(w_c) × 1920(h_c)
        //数字人视频的尺寸368(w_k) × 640(h_k) ---需要从表中取数字人的宽和高
        //y = (h_c - (w_c / w_k * h_k)) * 0.5
        //y = (height - (width/dpWidth * dpHight)) * 0.5   (1920-(1080/368 * 640)) * 0.5

        Integer dpWidth = 1080;
        Integer dpHight = 1920;

        // 根据isCommonDp判断是否设置figure_type
        if (videoTemplateRequest.getIsCommonDp() != null && videoTemplateRequest.getIsCommonDp() == 1) {
            person.put("figure_type", "whole_body");
        }else{
            //如果是定制数字人，则从eb_client_custom_digital_person 表中取结果 dpManLinkId
            CustomDigitalPerson customDigitalPerson = customDigitalPersonService.lambdaQuery()
                    .eq(CustomDigitalPerson::getLinkId, videoTemplateRequest.getDpManLinkId())
//                    .eq(CustomDigitalPerson::getUserId, videoTemplateRequest.getDpManLinkId())
                    .one();

            dpWidth = customDigitalPerson.getWidth();
            dpHight = customDigitalPerson.getHeight();
        }

        Integer temp = (int) Math.round((height - ((double)width/dpWidth * dpHight)) * 0.5);
        Integer y = (int) Math.round(temp);

        Integer heightRes = (int) Math.round((double)width/dpWidth * dpHight);
        if (heightRes % 2 != 0) {
            heightRes = heightRes + 1;
        }

        person.put("y", y);
        person.put("height", heightRes);

        params.put("person", person);

        // 构建字幕显示字段
//        Map<String, Object> subtitleConfig = new HashMap<>();
//        subtitleConfig.put("show", true);
//
//        params.put("subtitle_config", subtitleConfig);

        // 构建audio对象
        Map<String, Object> audio = new HashMap<>();
        audio.put("type", type);
        audio.put("volume", 100);

        // 根据type判断是否设置tts或wav_url
        if ("tts".equalsIgnoreCase(type) && videoTemplateRequest.getContent() != null) {
            Map<String, Object> tts = new HashMap<>();
            // 将text改为数组形式
            List<String> textArray = new ArrayList<>();
            textArray.add(videoTemplateRequest.getContent());
            tts.put("text", textArray);
            // 如果type=tts，则添加audio_man参数
            if (videoTemplateRequest.getAudioManLinkId() != null && !videoTemplateRequest.getAudioManLinkId().isEmpty()) {
                tts.put("audio_man", videoTemplateRequest.getAudioManLinkId());
                tts.put("speed", 1.1);
                tts.put("pitch", 1);
            }
            audio.put("tts", tts);
        }

        params.put("audio", audio);

        return params;
    }

    /**
     * 合成声音参数构建
     * @return
     */
    private Map<String, Object> createDpVoiceParams(VideoTemplateRequest videoTemplateRequest) {
        // 构建请求参数
        Map<String, Object> params = new HashMap<>();
        params.put("audio_man", videoTemplateRequest.getAudioManLinkId());
        params.put("speed", 1.1);
        params.put("pitch", 1);

        // 设置文本内容
        if (videoTemplateRequest.getContent() != null && !videoTemplateRequest.getContent().isEmpty()) {
            Map<String, Object> textMap = new HashMap<>();
            textMap.put("text", videoTemplateRequest.getContent());
            textMap.put("plain_text", videoTemplateRequest.getContent());
            params.put("text", textMap);
        }

        return params;
    }


    /**
     * 检测合成声音状态-》调用字幕agent和图片搜索
     * @param dpVoiceId
     * @param videoTemplateRequest
     * @return
     */
    private CompletableFuture<Map<String, Object>> scheduleVoiceStatusCheck(Integer dpVoiceId, Integer finalId, VideoTemplateRequest videoTemplateRequest) {
        CompletableFuture<Map<String, Object>> future = new CompletableFuture<>();

        log.info("模板成片-进入scheduleVoiceStatusCheck方法，dpVoiceId: {}", dpVoiceId);
        final Integer finalVoiceId = dpVoiceId;
        final Integer templateFinalId = finalId;
        final int[] retryCount = {0};
        Runnable checkTask = new Runnable() {
            @Override
            public void run() {
                DpVoice record = dpVoiceService.getById(dpVoiceId);
                if (record == null) {
                    log.warn("定时任务-未找到合成声音记录，linkId={}", finalVoiceId);
                    return;
                }

                //模板成片的记录，需要在下面步骤中更新相应字段值
                VideoTemplateFinal videoTemplateFinalRes = videoTemplateFinalService.getById(templateFinalId);
                Integer userId = videoTemplateFinalRes.getUserId();
                Integer merId = videoTemplateFinalRes.getMerId();

                if (record.getStatus() != null && record.getStatus() != 1 && record.getOssUrl() != null && !record.getOssUrl().isEmpty() && record.getDuration() != null) {
                    DpVoice recordNew = dpVoiceService.getById(dpVoiceId);
                    /**
                     * STEP3: 调用字幕agent
                     */
                    //根据语音获取字幕内容
                    List<Map<String, Object>> voiceContent = recordNew.getSubtitles();

                    log.info("模板成片-STEP【调用字幕agent】开始");
                    //对蝉镜返回的字幕进行格式化
                    StringBuilder sb = new StringBuilder();
                    for (Map<String, Object> item : voiceContent){
                        // String objectName = String.format("%s/%d/%d/%s/%s", rootFolder, merId, userId, folderName, fileName);
                        //[00:00 - 00:04]: 一盏离愁 孤单伫立在窗口
                        String formatString = String.format("[%s - %s]: %s  ",item.get("start_time").toString(),item.get("end_time").toString(),item.get("subtitle").toString());
                        sb.append(formatString);
                    }
                    String voiceContentString = sb.toString();

                    log.info("模板成片-传给agent的字幕为：{}", voiceContentString);

                    //传入蝉镜返回的字幕，调用字幕agent返回字幕内容和需要搜索的图片
                    Map<String , Object> subtitleAgentApi = videoSubtitleAgentApiNew(voiceContentString);
                    log.info("模板成片-STEP【调用字幕agent】成功, id:{}，数据为 {}", templateFinalId, subtitleAgentApi);

                    String subtitleMsg = (String) subtitleAgentApi.get("msg");
                    if (subtitleMsg != null) {
                        log.error("模板成片-STEP【调用字幕agent】失败, id:{}, 失败原因：{}", templateFinalId, subtitleMsg);
                        videoTemplateFinalRes.setStatus(4);
                        videoTemplateFinalRes.setErrMsg(subtitleMsg);
                        videoTemplateFinalService.updateById(videoTemplateFinalRes);

                        //推送ws事件
                        String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                        pushWsEvent(templateFinalId, userId, 4, eventName);

                        return;
                    }

                    //字幕，扣点
                    clientLoginService.updateMerPoint(userId,
                            merId,
                            "subtitleAgent",
                            templateFinalId,
                            agentClientService.getMouleName("subtitleAgent"),
                            "",
                            agentClientService.getPointByDuration("subtitleAgent", null),
                            null
                    );

                    videoTemplateFinalRes.setSubtitleAgentApi(subtitleAgentApi);
                    //获取标题字段
                    Map<String, Object> titleConent = (Map<String, Object>)subtitleAgentApi.get("title");

                    List<String> highlights = (List<String>)titleConent.get("highlight");
                    String contentHighlights = (String)titleConent.get("content");
                    String titleRes = FileUtil.processHighlightText(contentHighlights, highlights);

                    videoTemplateFinalRes.setTitle(titleRes);

                    //设置最终合成的视频时长
                    videoTemplateFinalRes.setDuration(recordNew.getDuration());
                    videoTemplateFinalService.updateById(videoTemplateFinalRes);

                    /**
                     * STEP4: 调用图片搜索
                     */
                    List<Map<String, Object>> picsKeywords = (List<Map<String, Object>>)subtitleAgentApi.get("pictureInPicture");
                    List<Map<String, String>> pictureInPicture = picSearchApi(picsKeywords);
                    log.info("搜索图片id={}, 结果为：{}", templateFinalId, pictureInPicture);

                    if (pictureInPicture == null || pictureInPicture.size() == 0) {
                        log.info("模板成片-STEP【调用图片搜索】失败, id:{}，参数为：{}", templateFinalId, picsKeywords);
//                        String picErrMsg = "搜索素材图片失败";
//                        videoTemplateFinalRes.setStatus(5);
//                        videoTemplateFinalRes.setErrMsg(picErrMsg);
//                        videoTemplateFinalService.updateById(videoTemplateFinalRes);

                        videoTemplateFinalRes.setImageUrls(new ArrayList<>());
                        videoTemplateFinalService.updateById(videoTemplateFinalRes);
//
//                        //推送ws事件
//                        String eventName = WebSocketEventType.AddVideoTemplateFail.name();
//                        pushWsEvent(templateFinalId, userId, 5, eventName);

//                        return;
                    }

                    if(pictureInPicture != null && !pictureInPicture.isEmpty()){
                        //更新图片字段
                        for (Map<String, String> picLoop : pictureInPicture){
                            String imgUrlLoop = picLoop.get("imgUrl");
                            String fileTypeLoop = FileUtil.getRealFileExtensionFromUrl(imgUrlLoop);
                            String fileNameloop = System.currentTimeMillis() + "." + fileTypeLoop;
                            String ossUrlLoop = OssUtil.uploadFromUrlToOss(imgUrlLoop, fileNameloop, userId, merId, "image", "keywordimg", "noasset");
                            picLoop.put("imgUrl", ossUrlLoop);
                        }
                        videoTemplateFinalRes.setImageUrls(pictureInPicture);
                        videoTemplateFinalService.updateById(videoTemplateFinalRes);

                        //图片搜索成功，扣点
                        clientLoginService.updateMerPoint(userId,
                                merId,
                                "picSearch",
                                templateFinalId,
                                agentClientService.getMouleName("picSearch"),
                                "",
                                agentClientService.getPointByDuration("picSearch", videoTemplateFinalRes.getDuration()),
                                videoTemplateFinalRes.getDuration()
                        );
                    }

                    log.info("模板成片-STEP【调用字幕agent结束】，id={}", templateFinalId);
                    HashMap<String, Object> result = new HashMap<>();
                    result.put("subtitleAgentApi", subtitleAgentApi);
                    result.put("pics", pictureInPicture);

                    future.complete(result);
                    return;
                }
                if (record.getStatus() != null && record.getStatus() == 1 && retryCount[0] < 5) {
                    retryCount[0]++;
                    log.info("模板成片-STEP【合成声音】未完成，1分钟后重试，voiceId={}, retry={}", finalVoiceId, retryCount[0]);
                    // 1分钟后重试
                    taskScheduler.schedule(this, new Date(System.currentTimeMillis() + 60 * 1000));
                } else if (record.getStatus() != null && record.getStatus() == 9 &&  (record.getErrMsg() != null || record.getErrReason() != null)) {
                    // 声音合成失败
                    videoTemplateFinalRes.setStatus(3);
                    videoTemplateFinalRes.setErrMsg(record.getErrMsg());
                    videoTemplateFinalService.updateById(videoTemplateFinalRes);
                    log.warn("模板成片-STEP【声音合成】失败，id={},失败原因为：{}", templateFinalId, record.getErrMsg());

                    //推送ws事件
                    String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                    pushWsEvent(templateFinalId, userId, 3, eventName);

                    return;
                }else if (retryCount[0] >= 5) {
                    // 声音合成失败
                    videoTemplateFinalRes.setStatus(3);
                    videoTemplateFinalRes.setErrMsg("声音合成状态获取失败，达到最大重试次数");
                    videoTemplateFinalService.updateById(videoTemplateFinalRes);
                    log.warn("模板成片-STEP【声音合成】失败，达到最大次数，id={}", templateFinalId);

                    //推送ws事件
                    String eventName = WebSocketEventType.AddVideoTemplateFail.name();
                    pushWsEvent(templateFinalId, userId, 3, eventName);

                    return;
                }
            }
        };
        // 首次1分钟后执行
        taskScheduler.schedule(checkTask, new Date(System.currentTimeMillis() + 60 * 1000));
        return future;
    }

    /**
     *    新建文件夹
     */
    @Override
    public boolean createFolder(CreateFolderVo vo) {
        Integer userId = vo.getUserId();
        Integer merId = vo.getMerId();
        Integer parentId = vo.getParentId() == null ? 0 : vo.getParentId();
        String name = vo.getName();

        // 新建文件夹：id为空
        VideoTemplateFinal videoTemplateFinal = new VideoTemplateFinal();
        videoTemplateFinal.setTitle(name);
        videoTemplateFinal.setParentId(parentId);
        videoTemplateFinal.setIsFolder(1);
        videoTemplateFinal.setIsDel(0);
        videoTemplateFinal.setUserId(userId);
        videoTemplateFinal.setMerId(merId);
        videoTemplateFinal.setCreateTime(new Date());
        videoTemplateFinal.setUpdateTime(new Date());
        // 处理completeFolder
        String completeFolder = "/";
        if (parentId != null && parentId != 0) {
            VideoTemplateFinal parent = videoTemplateFinalService.getById(parentId);
            if (parent == null) throw new CrmebException("父级文件夹不存在");

            //TODO 4
            if (parent.getUserId() != userId) throw new CrmebException("您无权进行该操作");
            if (parent.getIsFolder() == 0) throw new CrmebException("不能移动到非文件夹下");

            completeFolder = parent.getCompleteFolder() + parent.getId() + "/";
        }
        videoTemplateFinal.setCompleteFolder(completeFolder);
        videoTemplateFinalService.save(videoTemplateFinal);
        return true;
    }

    @Override
    public boolean renameFile(CreateFolderVo vo) {
        Integer userId = vo.getUserId();
        String name = vo.getName();
        Integer id = vo.getId();

        if(name == null || name.isEmpty()) throw new CrmebException("请输入要改的名字");
        if(id == null || id == 0) throw new CrmebException("请选择要改名的文件");

        VideoTemplateFinal videoTemplateFinal = videoTemplateFinalService.getById(id);
        if (videoTemplateFinal == null) throw new CrmebException("要重命名的文件夹不存在");
        if (videoTemplateFinal.getUserId() != userId) throw new CrmebException("您无权进行该操作");

        videoTemplateFinal.setTitle(name);
        videoTemplateFinal.setUpdateTime(new Date());
        videoTemplateFinalService.updateById(videoTemplateFinal);
        return true;
    }

    //移动文件夹
    @Override
    public boolean moveFolder(CreateFolderVo vo) {
        Integer userId = vo.getUserId();
        Integer merId = vo.getMerId();
        Integer parentId = vo.getParentId() == null ? 0 : vo.getParentId();

        Integer id = vo.getId();

        // 移动文件夹：name为空且parentId不为空且id不为空
        if(id == null) throw new CrmebException("请选择要移动的文件或者文件夹");

        VideoTemplateFinal folder = videoTemplateFinalService.getById(id);
        if (folder == null) throw new CrmebException("要移动的文件夹不存在");
        //TODO 2
        if (folder.getUserId() != userId) throw new CrmebException("您无权进行该操作");

        String completeFolderOld = folder.getCompleteFolder();
        String oldPath = completeFolderOld + id + "/";

        // 新父级
        String completeFolder = "/";
        if (parentId != 0) {
            VideoTemplateFinal parent = videoTemplateFinalService.getById(parentId);
            if (parent == null) throw new CrmebException("父级文件夹不存在");

            if (parent.getUserId() != userId) throw new CrmebException("您无权进行该操作");
            if (parent.getIsFolder() == 0) throw new CrmebException("请对文件夹进行操作");

            completeFolder = parent.getCompleteFolder() + parent.getId() + "/";
        }

        //TODO 3
        if (folder.getIsFolder() == 1){
            // 只有原文件是文件夹类型，才更新所有子文件夹和文件的complete_folder
            UpdateWrapper<VideoTemplateFinal> updateWrapper = new UpdateWrapper<>();
            updateWrapper.likeRight("complete_folder", oldPath);
            updateWrapper.setSql("complete_folder = CONCAT('" + completeFolder + id + "/', SUBSTRING(complete_folder, " + (oldPath.length() + 1) + "))");
            videoTemplateFinalService.update(null, updateWrapper);
        }
        // 同时更新当前文件夹的parentId和complete_folder
        folder.setParentId(parentId);
        folder.setCompleteFolder(completeFolder);
        folder.setUpdateTime(new Date());
        videoTemplateFinalService.updateById(folder);
        return true;
    }

    @Override
    public boolean batchMoveFolder(CreateFolderVo vo) {
        List<Integer> ids = vo.getIds();
        if (ids == null || ids.isEmpty()) {
            return false;
        }

        Integer userId = vo.getUserId();
        Integer parentId = vo.getParentId();

        // 验证目标父级文件夹
        if (parentId != null && parentId != 0) {
            VideoTemplateFinal parent = videoTemplateFinalService.getById(parentId);
            if (parent == null) {
                throw new CrmebException("目标父级文件夹不存在");
            }
            if (!userId.equals(parent.getUserId())) {
                throw new CrmebException("无权操作目标文件夹");
            }
            if (parent.getIsFolder() == null || parent.getIsFolder() == 0) {
                throw new CrmebException("目标必须是文件夹");
            }
        }

        String completeFolderNew = "/";
        if (parentId != null && parentId != 0) {
            VideoTemplateFinal parent = videoTemplateFinalService.getById(parentId);
            completeFolderNew = parent.getCompleteFolder() + parent.getId() + "/";
        }

        for (Integer id : ids) {
            try {
                VideoTemplateFinal folder = videoTemplateFinalService.getById(id);
                if (folder == null) {
                    throw new CrmebException("文件夹ID " + id + " 不存在");
                }
                if (!userId.equals(folder.getUserId())) {
                    throw new CrmebException("无权操作文件夹ID " + id);
                }

                String completeFolderOld = folder.getCompleteFolder();
                String oldPath = completeFolderOld + id + "/";

                if (folder.getIsFolder() == 1){
                    // 更新所有子文件夹和文件的complete_folder
                    UpdateWrapper<VideoTemplateFinal> updateWrapper = new UpdateWrapper<>();
                    updateWrapper.likeRight("complete_folder", oldPath);
                    updateWrapper.setSql("complete_folder = CONCAT('" + completeFolderNew + id + "/', SUBSTRING(complete_folder, " + (oldPath.length() + 1) + "))");
                    videoTemplateFinalService.update(null, updateWrapper);
                }

                // 更新当前文件夹
                folder.setParentId(parentId);
                folder.setCompleteFolder(completeFolderNew);
                folder.setUpdateTime(new Date());
                videoTemplateFinalService.updateById(folder);

            } catch (Exception e) {
                throw new CrmebException("移动文件夹ID " + id + " 失败: " + e.getMessage());
            }
        }

        return true;
    }


    /**
     * 批量删除素材
     * @param ids
     * @return
     */
    @Override
    public boolean batchDelVideo(List<Integer> ids) {
        if (ids == null || ids.isEmpty()) {
            return false;
        }

        //要批量删除OSS文件，减少OSS删除文件的请求
        List<String> fileList = new ArrayList<>();

        for (Integer id : ids) {
            try {
                List<String> result = delVideo(id, fileList);
                fileList = result;
            } catch (Exception e) {
                // 记录错误但继续处理其他ID
                System.err.println("删除素材ID " + id + " 失败: " + e.getMessage());
            }
        }

        //这里调用OSS的批量删除接口
        log.info("要删除的文件为：{}", fileList);
        // 如果列表不为空，异步删除OSS文件
        if (!fileList.isEmpty()) {
            final List<String> finalFileList = fileList;
            CompletableFuture.runAsync(() -> {
                try {
                    OssUtil.deleteMultiFileFromUrls(finalFileList);
                    log.info("异步删除OSS文件成功: {}", finalFileList);
                } catch (Exception e) {
                    log.error("异步删除OSS文件失败: {}, 错误信息: {}", finalFileList, e.getMessage());
                }
            });
        }

        return true;
    }

    /**
     * 删除
     * @param id
     * @return
     */
    private List<String> delVideo(Integer id, List<String> fileList) {
        VideoTemplateFinal videoTemplateFinal = videoTemplateFinalService.getById(id);
        if (videoTemplateFinal == null) throw new CrmebException("素材不存在");
        // 判断是否为文件夹
        if (videoTemplateFinal.getIsFolder() != null && videoTemplateFinal.getIsFolder() == 1) {
            // 文件夹，删除所有子项
            String delFolderId = videoTemplateFinal.getCompleteFolder() + id + "/";
            UpdateWrapper<VideoTemplateFinal> updateWrapper = new UpdateWrapper<>();
            updateWrapper.likeRight("complete_folder", delFolderId);

            // 添加符合条件的is_folder=0记录的id到fileList
            List<VideoTemplateFinal> childItems = videoTemplateFinalService.list(updateWrapper);
            for (VideoTemplateFinal childItem : childItems) {
                if (childItem.getIsFolder() != null && childItem.getIsFolder() == 0) {
                    if (childItem.getOssUrl() != null && !childItem.getOssUrl().isEmpty()) {
                        fileList.add(childItem.getOssUrl());
                    }
                }
            }

            updateWrapper.set("is_del", 1);
            videoTemplateFinalService.update(null, updateWrapper);
        }else{
            if (videoTemplateFinal.getOssUrl() != null && !videoTemplateFinal.getOssUrl().isEmpty()) {
                fileList.add(videoTemplateFinal.getOssUrl());
            }
        }
        // 删除自身
        videoTemplateFinal.setIsDel(1);
        videoTemplateFinal.setUpdateTime(new Date());
        videoTemplateFinalService.removeById(videoTemplateFinal);

        return fileList;
    }
} 